;
+ handler: RequestHandlerEnhanced<
+ P,
+ Q,
+ B,
+ // request.method's type contains way more verbs than we currently support
+ typeof request.method extends RouteMethod ? typeof request.method : any
+ >;
routeSchemas?: RouteValidator;
}) {
- let kibanaRequest: KibanaRequest
;
+ let kibanaRequest: KibanaRequest<
+ P,
+ Q,
+ B,
+ typeof request.method extends RouteMethod ? typeof request.method : any
+ >;
const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit);
try {
- kibanaRequest = CoreKibanaRequest.from(request, routeSchemas) as KibanaRequest<
- P,
- Q,
- B,
- typeof request.method
- >;
+ kibanaRequest = CoreKibanaRequest.from(request, routeSchemas);
} catch (error) {
this.logError('400 Bad Request', 400, { request, error });
return hapiResponseAdapter.toBadRequest(error.message);
diff --git a/packages/core/http/core-http-server-internal/src/cookie_session_storage.ts b/packages/core/http/core-http-server-internal/src/cookie_session_storage.ts
index d10c834f6bc8a..722ba968e56b1 100644
--- a/packages/core/http/core-http-server-internal/src/cookie_session_storage.ts
+++ b/packages/core/http/core-http-server-internal/src/cookie_session_storage.ts
@@ -121,12 +121,12 @@ export async function createCookieSessionStorageFactory(
}
},
},
- validateFunc: async (req: Request, session: T | T[]) => {
+ validate: async (req: Request, session: T | T[]) => {
const result = cookieOptions.validate(session);
if (!result.isValid) {
clearInvalidCookie(req, result.path);
}
- return { valid: result.isValid };
+ return { isValid: result.isValid };
},
});
diff --git a/packages/core/http/core-http-server-internal/src/http_server.ts b/packages/core/http/core-http-server-internal/src/http_server.ts
index 4f3c96518cefc..46470bac7c504 100644
--- a/packages/core/http/core-http-server-internal/src/http_server.ts
+++ b/packages/core/http/core-http-server-internal/src/http_server.ts
@@ -15,7 +15,6 @@ import { createServer, getServerOptions, setTlsConfig, getRequestId } from '@kbn
import type { Duration } from 'moment';
import { Observable, Subscription, firstValueFrom, pairwise, take } from 'rxjs';
import apm from 'elastic-apm-node';
-// @ts-expect-error no type definition
import Brok from 'brok';
import type { Logger, LoggerFactory } from '@kbn/logging';
import type { InternalExecutionContextSetup } from '@kbn/core-execution-context-server-internal';
@@ -722,7 +721,6 @@ export class HttpServer {
// validation applied in ./http_tools#getServerOptions
// (All NP routes are already required to specify their own validation in order to access the payload)
validate,
- // @ts-expect-error Types are outdated and doesn't allow `payload.multipart` to be `true`
payload: [allow, override, maxBytes, output, parse, timeout?.payload].some(
(x) => x !== undefined
)
diff --git a/packages/deeplinks/security/deep_links.ts b/packages/deeplinks/security/deep_links.ts
index c7d5b54fb202a..54b18dcaf9206 100644
--- a/packages/deeplinks/security/deep_links.ts
+++ b/packages/deeplinks/security/deep_links.ts
@@ -87,5 +87,5 @@ export enum SecurityPageName {
entityAnalyticsManagement = 'entity_analytics-management',
entityAnalyticsAssetClassification = 'entity_analytics-asset-classification',
coverageOverview = 'coverage-overview',
- notesManagement = 'notes-management',
+ notes = 'notes',
}
diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts
index d611dd0df138c..6dcd7d95dfda8 100644
--- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts
+++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts
@@ -12,6 +12,7 @@
import * as rt from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { AlertSchema } from './alert_schema';
+import { EcsSchema } from './ecs_schema';
import { LegacyAlertSchema } from './legacy_alert_schema';
const ISO_DATE_PATTERN = /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z$/;
export const IsoDateString = new rt.Type(
@@ -88,6 +89,6 @@ const ObservabilitySloAlertOptional = rt.partial({
});
// prettier-ignore
-export const ObservabilitySloAlertSchema = rt.intersection([ObservabilitySloAlertRequired, ObservabilitySloAlertOptional, AlertSchema, LegacyAlertSchema]);
+export const ObservabilitySloAlertSchema = rt.intersection([ObservabilitySloAlertRequired, ObservabilitySloAlertOptional, AlertSchema, EcsSchema, LegacyAlertSchema]);
// prettier-ignore
export type ObservabilitySloAlert = rt.TypeOf;
diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_uptime_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_uptime_schema.ts
index db6844688dbc6..6a746f722d612 100644
--- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_uptime_schema.ts
+++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_uptime_schema.ts
@@ -88,13 +88,15 @@ const ObservabilityUptimeAlertOptional = rt.partial({
value: schemaStringArray,
})
),
- 'location.id': schemaString,
- 'location.name': schemaString,
+ 'location.id': schemaStringArray,
+ 'location.name': schemaStringArray,
'monitor.id': schemaString,
'monitor.name': schemaString,
+ 'monitor.state.id': schemaString,
'monitor.tags': schemaStringArray,
'monitor.type': schemaString,
- 'observer.geo.name': schemaString,
+ 'observer.geo.name': schemaStringArray,
+ 'observer.name': schemaStringArray,
'tls.server.hash.sha256': schemaString,
'tls.server.x509.issuer.common_name': schemaString,
'tls.server.x509.not_after': schemaDate,
diff --git a/packages/kbn-search-index-documents/components/document_list.tsx b/packages/kbn-search-index-documents/components/document_list.tsx
index ec9efe7d6b1d7..5491d228e7d07 100644
--- a/packages/kbn-search-index-documents/components/document_list.tsx
+++ b/packages/kbn-search-index-documents/components/document_list.tsx
@@ -9,7 +9,7 @@
import React, { useState } from 'react';
-import { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types';
+import type { IndicesGetMappingResponse, SearchHit } from '@elastic/elasticsearch/lib/api/types';
import {
EuiButtonEmpty,
@@ -30,18 +30,22 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react';
-import { resultMetaData, resultToField } from './result/result_metadata';
+import { resultMetaData, resultToFieldFromMappingResponse } from './result/result_metadata';
import { Result } from '..';
+import { type ResultProps } from './result/result';
+
interface DocumentListProps {
dataTelemetryIdPrefix: string;
docs: SearchHit[];
docsPerPage: number;
isLoading: boolean;
- mappings: Record | undefined;
+ mappings: IndicesGetMappingResponse | undefined;
meta: Pagination;
onPaginate: (newPageIndex: number) => void;
- setDocsPerPage: (docsPerPage: number) => void;
+ setDocsPerPage?: (docsPerPage: number) => void;
+ onDocumentClick?: (doc: SearchHit) => void;
+ resultProps?: Partial;
}
export const DocumentList: React.FC = ({
@@ -53,6 +57,8 @@ export const DocumentList: React.FC = ({
meta,
onPaginate,
setDocsPerPage,
+ onDocumentClick,
+ resultProps = {},
}) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@@ -99,7 +105,12 @@ export const DocumentList: React.FC = ({
{docs.map((doc) => {
return (
-
+ onDocumentClick(doc) : undefined}
+ {...resultProps}
+ />
);
@@ -116,81 +127,83 @@ export const DocumentList: React.FC = ({
onPageClick={onPaginate}
/>
-
- {
- setIsPopoverOpen(true);
- }}
- >
- {i18n.translate('searchIndexDocuments.documentList.pagination.itemsPerPage', {
- defaultMessage: 'Documents per page: {docPerPage}',
- values: { docPerPage: docsPerPage },
- })}
-
- }
- isOpen={isPopoverOpen}
- closePopover={() => {
- setIsPopoverOpen(false);
- }}
- panelPaddingSize="none"
- anchorPosition="downLeft"
- >
-
+ {
- setIsPopoverOpen(false);
- setDocsPerPage(10);
+ setIsPopoverOpen(true);
}}
>
- {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', {
- defaultMessage: '{docCount} documents',
- values: { docCount: 10 },
+ {i18n.translate('searchIndexDocuments.documentList.pagination.itemsPerPage', {
+ defaultMessage: 'Documents per page: {docPerPage}',
+ values: { docPerPage: docsPerPage },
})}
- ,
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={() => {
+ setIsPopoverOpen(false);
+ }}
+ panelPaddingSize="none"
+ anchorPosition="downLeft"
+ >
+ {
+ setIsPopoverOpen(false);
+ setDocsPerPage(10);
+ }}
+ >
+ {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', {
+ defaultMessage: '{docCount} documents',
+ values: { docCount: 10 },
+ })}
+ ,
- {
- setIsPopoverOpen(false);
- setDocsPerPage(25);
- }}
- >
- {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', {
- defaultMessage: '{docCount} documents',
- values: { docCount: 25 },
- })}
- ,
- {
- setIsPopoverOpen(false);
- setDocsPerPage(50);
- }}
- >
- {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', {
- defaultMessage: '{docCount} documents',
- values: { docCount: 50 },
- })}
- ,
- ]}
- />
-
-
+ {
+ setIsPopoverOpen(false);
+ setDocsPerPage(25);
+ }}
+ >
+ {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', {
+ defaultMessage: '{docCount} documents',
+ values: { docCount: 25 },
+ })}
+ ,
+ {
+ setIsPopoverOpen(false);
+ setDocsPerPage(50);
+ }}
+ >
+ {i18n.translate('searchIndexDocuments.documentList.paginationOptions.option', {
+ defaultMessage: '{docCount} documents',
+ values: { docCount: 50 },
+ })}
+ ,
+ ]}
+ />
+
+
+ )}
diff --git a/packages/kbn-search-index-documents/components/documents_list.test.tsx b/packages/kbn-search-index-documents/components/documents_list.test.tsx
index b445c5fa48711..b97b36989ad62 100644
--- a/packages/kbn-search-index-documents/components/documents_list.test.tsx
+++ b/packages/kbn-search-index-documents/components/documents_list.test.tsx
@@ -58,8 +58,14 @@ describe('DocumentList', () => {
},
],
mappings: {
- AvgTicketPrice: {
- type: 'float' as const,
+ kibana_sample_data_flights: {
+ mappings: {
+ properties: {
+ AvgTicketPrice: {
+ type: 'float' as const,
+ },
+ },
+ },
},
},
};
diff --git a/packages/kbn-search-index-documents/components/result/index.ts b/packages/kbn-search-index-documents/components/result/index.ts
index a5e613fbd83ec..8c894f7e2ca3b 100644
--- a/packages/kbn-search-index-documents/components/result/index.ts
+++ b/packages/kbn-search-index-documents/components/result/index.ts
@@ -8,4 +8,8 @@
*/
export { Result } from './result';
-export { resultMetaData, resultToField } from './result_metadata';
+export {
+ resultMetaData,
+ resultToFieldFromMappingResponse,
+ resultToFieldFromMappings as resultToField,
+} from './result_metadata';
diff --git a/packages/kbn-search-index-documents/components/result/result.tsx b/packages/kbn-search-index-documents/components/result/result.tsx
index 4e6f0ed8c6eb8..5e1c4db104116 100644
--- a/packages/kbn-search-index-documents/components/result/result.tsx
+++ b/packages/kbn-search-index-documents/components/result/result.tsx
@@ -9,76 +9,147 @@
import React, { useState } from 'react';
-import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiToolTip } from '@elastic/eui';
+import {
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPanel,
+ EuiSpacer,
+ EuiToolTip,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ResultFields } from './results_fields';
-import { ResultHeader } from './result_header';
import './result.scss';
import { MetaDataProps, ResultFieldProps } from './result_types';
+import { RichResultHeader } from './rich_result_header';
+import { ResultHeader } from './result_header';
+
+export const DEFAULT_VISIBLE_FIELDS = 3;
-interface ResultProps {
+export interface ResultProps {
fields: ResultFieldProps[];
metaData: MetaDataProps;
+ defaultVisibleFields?: number;
+ showScore?: boolean;
+ compactCard?: boolean;
+ onDocumentClick?: () => void;
}
-export const Result: React.FC = ({ metaData, fields }) => {
+export const Result: React.FC = ({
+ metaData,
+ fields,
+ defaultVisibleFields = DEFAULT_VISIBLE_FIELDS,
+ compactCard = true,
+ showScore = false,
+ onDocumentClick,
+}) => {
const [isExpanded, setIsExpanded] = useState(false);
const tooltipText =
- fields.length <= 3
+ fields.length <= defaultVisibleFields
? i18n.translate('searchIndexDocuments.result.expandTooltip.allVisible', {
defaultMessage: 'All fields are visible',
})
: isExpanded
? i18n.translate('searchIndexDocuments.result.expandTooltip.showFewer', {
defaultMessage: 'Show {amount} fewer fields',
- values: { amount: fields.length - 3 },
+ values: { amount: fields.length - defaultVisibleFields },
})
: i18n.translate('searchIndexDocuments.result.expandTooltip.showMore', {
defaultMessage: 'Show {amount} more fields',
- values: { amount: fields.length - 3 },
+ values: { amount: fields.length - defaultVisibleFields },
});
const toolTipContent = <>{tooltipText}>;
return (
-
+
-
+
-
+ {compactCard && (
+
+ )}
+ {!compactCard && (
+
+
+ ) => {
+ e.stopPropagation();
+ setIsExpanded(!isExpanded);
+ }}
+ aria-label={tooltipText}
+ />
+
+
+ }
+ />
+ )}
+ {!compactCard &&
+ ((isExpanded && fields.length > 0) ||
+ (!isExpanded && fields.slice(0, defaultVisibleFields).length > 0)) && (
+
+ )}
-
-
-
- setIsExpanded(!isExpanded)}
- aria-label={tooltipText}
- />
-
-
-
+ {compactCard && (
+
+
+
+ ) => {
+ e.stopPropagation();
+ setIsExpanded(!isExpanded);
+ }}
+ aria-label={tooltipText}
+ />
+
+
+
+ )}
);
diff --git a/packages/kbn-search-index-documents/components/result/result_metadata.ts b/packages/kbn-search-index-documents/components/result/result_metadata.ts
index 783cd537b4535..ba50644cafc59 100644
--- a/packages/kbn-search-index-documents/components/result/result_metadata.ts
+++ b/packages/kbn-search-index-documents/components/result/result_metadata.ts
@@ -7,7 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import type { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types';
+import type {
+ IndicesGetMappingResponse,
+ MappingProperty,
+ SearchHit,
+} from '@elastic/elasticsearch/lib/api/types';
import type { MetaDataProps } from './result_types';
const TITLE_KEYS = ['title', 'name'];
@@ -37,15 +41,19 @@ export const resultTitle = (result: SearchHit): string | undefined => {
export const resultMetaData = (result: SearchHit): MetaDataProps => ({
id: result._id!,
title: resultTitle(result),
+ score: result._score,
});
-export const resultToField = (result: SearchHit, mappings?: Record) => {
- if (mappings && result._source && !Array.isArray(result._source)) {
+export const resultToFieldFromMappingResponse = (
+ result: SearchHit,
+ mappings?: IndicesGetMappingResponse
+) => {
+ if (mappings && mappings[result._index] && result._source && !Array.isArray(result._source)) {
if (typeof result._source === 'object') {
return Object.entries(result._source).map(([key, value]) => {
return {
fieldName: key,
- fieldType: mappings[key]?.type ?? 'object',
+ fieldType: mappings[result._index]?.mappings?.properties?.[key]?.type ?? 'object',
fieldValue: JSON.stringify(value, null, 2),
};
});
@@ -53,3 +61,19 @@ export const resultToField = (result: SearchHit, mappings?: Record
+) => {
+ if (mappings && result._source && !Array.isArray(result._source)) {
+ return Object.entries(result._source).map(([key, value]) => {
+ return {
+ fieldName: key,
+ fieldType: mappings[key]?.type ?? 'object',
+ fieldValue: JSON.stringify(value, null, 2),
+ };
+ });
+ }
+ return [];
+};
diff --git a/packages/kbn-search-index-documents/components/result/result_types.ts b/packages/kbn-search-index-documents/components/result/result_types.ts
index e04ccb1091c8e..c7899874b27ee 100644
--- a/packages/kbn-search-index-documents/components/result/result_types.ts
+++ b/packages/kbn-search-index-documents/components/result/result_types.ts
@@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
import { IconType } from '@elastic/eui';
export interface ResultFieldProps {
@@ -20,4 +21,6 @@ export interface MetaDataProps {
id: string;
onDocumentDelete?: Function;
title?: string;
+ score?: SearchHit['_score'];
+ showScore?: boolean;
}
diff --git a/packages/kbn-search-index-documents/components/result/rich_result_header.tsx b/packages/kbn-search-index-documents/components/result/rich_result_header.tsx
new file mode 100644
index 0000000000000..7caff8514871f
--- /dev/null
+++ b/packages/kbn-search-index-documents/components/result/rich_result_header.tsx
@@ -0,0 +1,228 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import React, { useState } from 'react';
+
+import {
+ EuiButton,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiLink,
+ EuiPanel,
+ EuiPopover,
+ EuiPopoverFooter,
+ EuiPopoverTitle,
+ EuiText,
+ EuiTextColor,
+ EuiTitle,
+ useEuiTheme,
+} from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+import { css } from '@emotion/react';
+import { MetaDataProps } from './result_types';
+
+interface Props {
+ metaData: MetaDataProps;
+ title: string;
+ rightSideActions?: React.ReactNode;
+ showScore?: boolean;
+ onTitleClick?: () => void;
+}
+
+interface TermDef {
+ label: string | number;
+}
+
+const Term: React.FC = ({ label }) => (
+
+
+ {label}:
+
+
+);
+
+const Definition: React.FC = ({ label }) => (
+
+ {label}
+
+);
+const MetadataPopover: React.FC = ({
+ id,
+ onDocumentDelete,
+ score,
+ showScore = false,
+}) => {
+ const [popoverIsOpen, setPopoverIsOpen] = useState(false);
+ const closePopover = () => setPopoverIsOpen(false);
+
+ const metaDataIcon = (
+ ) => {
+ e.stopPropagation();
+ setPopoverIsOpen(!popoverIsOpen);
+ }}
+ aria-label={i18n.translate('searchIndexDocuments.result.header.metadata.icon.ariaLabel', {
+ defaultMessage: 'Metadata for document: {id}',
+ values: { id },
+ })}
+ />
+ );
+
+ return (
+
+
+ {i18n.translate('searchIndexDocuments.result.header.metadata.title', {
+ defaultMessage: 'Document metadata',
+ })}
+
+
+
+
+
+
+
+
+
+ {score && showScore && (
+
+
+
+
+
+
+ )}
+
+ {onDocumentDelete && (
+
+ ) => {
+ e.stopPropagation();
+ closePopover();
+ }}
+ fullWidth
+ >
+ {i18n.translate('searchIndexDocuments.result.header.metadata.deleteDocument', {
+ defaultMessage: 'Delete document',
+ })}
+
+
+ )}
+
+ );
+};
+
+const Score: React.FC<{ score: MetaDataProps['score'] }> = ({ score }) => {
+ return (
+
+
+
+
+
+
+
+
+ {score ? score.toString().substring(0, 5) : '-'}
+
+
+
+
+
+ );
+};
+
+export const RichResultHeader: React.FC = ({
+ title,
+ metaData,
+ rightSideActions = null,
+ showScore = false,
+ onTitleClick,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ return (
+
+
+ {showScore && (
+
+
+
+ )}
+
+
+
+
+
+
+ {onTitleClick ? (
+
+
+ {title}
+
+
+ ) : (
+
+ {title}
+
+ )}
+
+ {!!metaData && (
+
+
+
+ )}
+
+
+
+
+
+ {rightSideActions}
+
+
+ );
+};
diff --git a/packages/kbn-securitysolution-es-utils/src/transform_error/index.ts b/packages/kbn-securitysolution-es-utils/src/transform_error/index.ts
index c9882bbd456d9..3b2f15b6af7cf 100644
--- a/packages/kbn-securitysolution-es-utils/src/transform_error/index.ts
+++ b/packages/kbn-securitysolution-es-utils/src/transform_error/index.ts
@@ -8,7 +8,7 @@
*/
import { errors } from '@elastic/elasticsearch';
-import Boom from '@hapi/boom';
+import type { Boom } from '@hapi/boom';
import { stringifyZodError } from '@kbn/zod-helpers';
import { ZodError } from '@kbn/zod';
import { BadRequestError } from '../bad_request_error';
@@ -18,8 +18,15 @@ export interface OutputError {
statusCode: number;
}
+// We can't import `isBoom` from @hapi/boom today because we get transpilation errors in Webpack 4
+// due to the usage of the operator ?? inside the `@hapi/boom` library and its dependencies.
+// TODO: Might be able to use the library's `isBoom` when Webpack 5 is merged (https://github.com/elastic/kibana/pull/191106)
+function isBoom(err: unknown): err is Boom {
+ return err instanceof Error && `isBoom` in err && !!err.isBoom;
+}
+
export const transformError = (err: Error & Partial): OutputError => {
- if (Boom.isBoom(err)) {
+ if (isBoom(err)) {
return {
message: err.output.payload.message,
statusCode: err.output.statusCode,
diff --git a/packages/kbn-server-http-tools/src/default_validation_error_handler.ts b/packages/kbn-server-http-tools/src/default_validation_error_handler.ts
index 02e93cfa93e7c..17ac3a4499fe9 100644
--- a/packages/kbn-server-http-tools/src/default_validation_error_handler.ts
+++ b/packages/kbn-server-http-tools/src/default_validation_error_handler.ts
@@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { Lifecycle, Request, ResponseToolkit, Util } from '@hapi/hapi';
+import { Lifecycle, Request, ResponseToolkit, Utils } from '@hapi/hapi';
import { ValidationError } from 'joi';
import Hoek from '@hapi/hoek';
@@ -17,7 +17,7 @@ import Hoek from '@hapi/hoek';
export interface HapiValidationError extends ValidationError {
output: {
statusCode: number;
- headers: Util.Dictionary;
+ headers: Utils.Dictionary;
payload: {
statusCode: number;
error: string;
diff --git a/renovate.json b/renovate.json
index eeb91efd871bf..fa304f97a0d6b 100644
--- a/renovate.json
+++ b/renovate.json
@@ -134,6 +134,15 @@
"minimumReleaseAge": "7 days",
"enabled": true
},
+ {
+ "groupName": "HAPI ecosystem",
+ "matchDepNames": ["@hapi/**", "brok"],
+ "reviewers": ["team:kibana-core"],
+ "matchBaseBranches": ["main"],
+ "labels": ["release_note:skip", "Team:Core", "backport:prev-minor"],
+ "minimumReleaseAge": "7 days",
+ "enabled": true
+ },
{
"groupName": "babel",
"matchDepNames": ["@types/babel__core"],
diff --git a/src/core/server/integration_tests/http/http_server.test.ts b/src/core/server/integration_tests/http/http_server.test.ts
index 6f54f3e0a5731..d6c422758a708 100644
--- a/src/core/server/integration_tests/http/http_server.test.ts
+++ b/src/core/server/integration_tests/http/http_server.test.ts
@@ -99,8 +99,8 @@ describe('Http server', () => {
expect(response.header.connection).toBe('close');
});
- test('any requests triggered while stopping should be rejected with 503', async () => {
- const [, , response] = await Promise.all([
+ test('any requests triggered while stopping should be rejected', async () => {
+ await Promise.all([
// Trigger a request that should hold the server from stopping until fulfilled (otherwise the server will stop straight away)
supertest(innerServerListener).post('/'),
// Stop the server while the request is in progress
@@ -111,16 +111,11 @@ describe('Http server', () => {
// Trigger a new request while shutting down (should be rejected)
(async () => {
await new Promise((resolve) => setTimeout(resolve, (2 * shutdownTimeout) / 3));
- return supertest(innerServerListener).post('/');
+ const request = supertest(innerServerListener).post('/');
+ await expect(request).rejects.toThrow('socket hang up');
+ await request.catch((err) => expect(err.code).toBe('ECONNRESET'));
})(),
]);
- expect(response.status).toBe(503);
- expect(response.body).toStrictEqual({
- statusCode: 503,
- error: 'Service Unavailable',
- message: 'Kibana is shutting down and not accepting new incoming requests',
- });
- expect(response.header.connection).toBe('close');
});
test('when no ongoing connections, the server should stop without waiting any longer', async () => {
diff --git a/src/plugins/interactive_setup/public/app.scss b/src/plugins/interactive_setup/public/app.scss
index 119a2377dd7d2..837a856610a23 100644
--- a/src/plugins/interactive_setup/public/app.scss
+++ b/src/plugins/interactive_setup/public/app.scss
@@ -9,10 +9,10 @@
}
.interactiveSetup__logo {
+ margin-bottom: $euiSizeXL;
+
@include kibanaCircleLogo;
@include euiBottomShadowMedium;
-
- margin-bottom: $euiSizeXL;
}
.interactiveSetup__content {
diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_row_indicator_provider.ts b/test/functional/apps/discover/context_awareness/extensions/_get_row_indicator_provider.ts
index f3f4cd40145e3..9f46b86daf3c4 100644
--- a/test/functional/apps/discover/context_awareness/extensions/_get_row_indicator_provider.ts
+++ b/test/functional/apps/discover/context_awareness/extensions/_get_row_indicator_provider.ts
@@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const dataViews = getService('dataViews');
+ // Failing: See https://github.com/elastic/kibana/issues/194043
// Failing: See https://github.com/elastic/kibana/issues/194043
describe.skip('extension getRowIndicatorProvider', () => {
before(async () => {
diff --git a/test/functional/fixtures/es_archiver/kibana_sample_data_logs_logsdb/data.json.gz b/test/functional/fixtures/es_archiver/kibana_sample_data_logs_logsdb/data.json.gz
new file mode 100644
index 0000000000000..349fa50d7989f
Binary files /dev/null and b/test/functional/fixtures/es_archiver/kibana_sample_data_logs_logsdb/data.json.gz differ
diff --git a/test/functional/fixtures/es_archiver/kibana_sample_data_logs_logsdb/mappings.json b/test/functional/fixtures/es_archiver/kibana_sample_data_logs_logsdb/mappings.json
new file mode 100644
index 0000000000000..6b3dea1e0d718
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/kibana_sample_data_logs_logsdb/mappings.json
@@ -0,0 +1,171 @@
+{
+ "type": "index",
+ "value": {
+ "aliases": {},
+ "index": "kibana_sample_data_logslogsdb",
+ "mappings": {
+ "_data_stream_timestamp": {
+ "enabled": true
+ },
+ "properties": {
+ "@timestamp": {
+ "type": "date"
+ },
+ "agent": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "bytes": {
+ "type": "long"
+ },
+ "bytes_counter": {
+ "time_series_metric": "counter",
+ "type": "long"
+ },
+ "bytes_gauge": {
+ "time_series_metric": "gauge",
+ "type": "long"
+ },
+ "clientip": {
+ "type": "ip"
+ },
+ "event": {
+ "properties": {
+ "dataset": {
+ "type": "keyword"
+ }
+ }
+ },
+ "extension": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "geo": {
+ "properties": {
+ "coordinates": {
+ "type": "geo_point"
+ },
+ "dest": {
+ "type": "keyword"
+ },
+ "src": {
+ "type": "keyword"
+ },
+ "srcdest": {
+ "type": "keyword"
+ }
+ }
+ },
+ "host": {
+ "properties": {
+ "name": {
+ "type": "keyword"
+ }
+ }
+ },
+ "index": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "ip": {
+ "type": "ip"
+ },
+ "machine": {
+ "properties": {
+ "os": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "ram": {
+ "type": "long"
+ }
+ }
+ },
+ "memory": {
+ "type": "double"
+ },
+ "message": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "phpmemory": {
+ "type": "long"
+ },
+ "referer": {
+ "type": "keyword"
+ },
+ "request": {
+ "time_series_dimension": true,
+ "type": "keyword"
+ },
+ "response": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "tags": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "timestamp": {
+ "path": "@timestamp",
+ "type": "alias"
+ },
+ "url": {
+ "time_series_dimension": true,
+ "type": "keyword"
+ },
+ "utc_time": {
+ "type": "date"
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "auto_expand_replicas": "0-1",
+ "mode": "time_series",
+ "number_of_replicas": "0",
+ "number_of_shards": "1",
+ "routing_path": "request",
+ "time_series": {
+ "end_time": "2023-06-28T09:17:00.283Z",
+ "start_time": "2023-03-28T09:17:00.283Z"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/functional/fixtures/kbn_archiver/kibana_sample_data_logs_logsdb.json b/test/functional/fixtures/kbn_archiver/kibana_sample_data_logs_logsdb.json
new file mode 100644
index 0000000000000..b5836dff4d717
--- /dev/null
+++ b/test/functional/fixtures/kbn_archiver/kibana_sample_data_logs_logsdb.json
@@ -0,0 +1,22 @@
+{
+ "attributes": {
+ "fieldFormatMap": "{\"hour_of_day\":{}}",
+ "name": "Kibana Sample Data Logs (LogsDB)",
+ "runtimeFieldMap": "{\"hour_of_day\":{\"type\":\"long\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getHour());\"}}}",
+ "timeFieldName": "timestamp",
+ "title": "kibana_sample_data_logslogsdb"
+ },
+ "coreMigrationVersion": "8.8.0",
+ "created_at": "2023-04-27T13:09:20.333Z",
+ "id": "90943e30-9a47-11e8-b64d-95841ca0c247",
+ "managed": false,
+ "references": [],
+ "sort": [
+ 1682600960333,
+ 64
+ ],
+ "type": "index-pattern",
+ "typeMigrationVersion": "7.11.0",
+ "updated_at": "2023-04-27T13:09:20.333Z",
+ "version": "WzIxLDFd"
+}
\ No newline at end of file
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 7a66911a4bee5..c0a3aed9ff3ac 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1306,6 +1306,8 @@
"@kbn/observability-plugin/*": ["x-pack/plugins/observability_solution/observability/*"],
"@kbn/observability-shared-plugin": ["x-pack/plugins/observability_solution/observability_shared"],
"@kbn/observability-shared-plugin/*": ["x-pack/plugins/observability_solution/observability_shared/*"],
+ "@kbn/observability-synthetics-test-data": ["x-pack/packages/observability/synthetics_test_data"],
+ "@kbn/observability-synthetics-test-data/*": ["x-pack/packages/observability/synthetics_test_data/*"],
"@kbn/observability-utils": ["x-pack/packages/observability/observability_utils"],
"@kbn/observability-utils/*": ["x-pack/packages/observability/observability_utils/*"],
"@kbn/oidc-provider-plugin": ["x-pack/test/security_api_integration/plugins/oidc_provider"],
diff --git a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts
index caf4ce56b3482..95337518361e9 100644
--- a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts
+++ b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts
@@ -265,6 +265,19 @@ export type InferenceServiceSettings =
url: string;
};
}
+ | {
+ service: 'alibabacloud-ai-search';
+ service_settings: {
+ api_key: string;
+ service_id: string;
+ host: string;
+ workspace: string;
+ http_schema: 'https' | 'http';
+ rate_limit: {
+ requests_per_minute: number;
+ };
+ };
+ }
| {
service: 'amazonbedrock';
service_settings: {
diff --git a/x-pack/packages/observability/synthetics_test_data/README.md b/x-pack/packages/observability/synthetics_test_data/README.md
new file mode 100644
index 0000000000000..922df9939572d
--- /dev/null
+++ b/x-pack/packages/observability/synthetics_test_data/README.md
@@ -0,0 +1,3 @@
+# @kbn/observability-synthetics-test-data
+
+Provides utilities to generate synthetics test data
diff --git a/x-pack/packages/observability/synthetics_test_data/index.ts b/x-pack/packages/observability/synthetics_test_data/index.ts
new file mode 100644
index 0000000000000..d1fe1034d7b1e
--- /dev/null
+++ b/x-pack/packages/observability/synthetics_test_data/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { makeUpSummary, makeDownSummary } from './src/make_summaries';
diff --git a/x-pack/packages/observability/synthetics_test_data/jest.config.js b/x-pack/packages/observability/synthetics_test_data/jest.config.js
new file mode 100644
index 0000000000000..62001f4072246
--- /dev/null
+++ b/x-pack/packages/observability/synthetics_test_data/jest.config.js
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../..',
+ roots: ['/x-pack/packages/observability/synthetics_test_data'],
+};
diff --git a/x-pack/packages/observability/synthetics_test_data/kibana.jsonc b/x-pack/packages/observability/synthetics_test_data/kibana.jsonc
new file mode 100644
index 0000000000000..94f80d9b59cad
--- /dev/null
+++ b/x-pack/packages/observability/synthetics_test_data/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-common",
+ "id": "@kbn/observability-synthetics-test-data",
+ "owner": "@elastic/obs-ux-management-team",
+}
diff --git a/x-pack/packages/observability/synthetics_test_data/package.json b/x-pack/packages/observability/synthetics_test_data/package.json
new file mode 100644
index 0000000000000..8ee7793e644ca
--- /dev/null
+++ b/x-pack/packages/observability/synthetics_test_data/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@kbn/observability-synthetics-test-data",
+ "descriptio": "Utils to generate observability synthetics test data",
+ "author": "UX Management",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0"
+}
\ No newline at end of file
diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/data/sample_docs.ts b/x-pack/packages/observability/synthetics_test_data/src/make_summaries.ts
similarity index 53%
rename from x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/data/sample_docs.ts
rename to x-pack/packages/observability/synthetics_test_data/src/make_summaries.ts
index 62e2013ce0ff8..a44ffc15c28ec 100644
--- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/data/sample_docs.ts
+++ b/x-pack/packages/observability/synthetics_test_data/src/make_summaries.ts
@@ -6,124 +6,39 @@
*/
import { v4 as uuidv4 } from 'uuid';
-import { getGeoData } from './browser_docs';
+import moment from 'moment';
+import { getGeoData } from './utils';
export interface DocOverrides {
timestamp?: string;
monitorId?: string;
name?: string;
testRunId?: string;
- locationName?: string;
+ location?: {
+ id: string;
+ label: string;
+ };
configId?: string;
}
-export const getUpHit = ({
+export const makeUpSummary = ({
name,
timestamp,
monitorId,
configId,
testRunId,
- locationName,
+ location,
}: DocOverrides = {}) => ({
- ...getGeoData(locationName),
+ ...getGeoData(location),
+ ...commons,
summary: {
up: 1,
down: 0,
final_attempt: true,
},
- tcp: {
- rtt: {
- connect: {
- us: 22245,
- },
- },
- },
- agent: {
- name: 'docker-fleet-server',
- id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
- type: 'heartbeat',
- ephemeral_id: '264bb432-93f6-4aa6-a14d-266c53b9e7c7',
- version: '8.7.0',
- },
- resolve: {
- rtt: {
- us: 3101,
- },
- ip: '142.250.181.196',
- },
- elastic_agent: {
- id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
- version: '8.7.0',
- snapshot: true,
- },
- monitor: {
- duration: {
- us: 155239,
- },
- ip: '142.250.181.196',
- origin: 'ui',
- name: name ?? 'Test Monitor',
- timespan: {
- lt: '2022-12-18T09:55:04.211Z',
- gte: '2022-12-18T09:52:04.211Z',
- },
- fleet_managed: true,
- id: monitorId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
- check_group: 'a039fd21-7eb9-11ed-8949-0242ac120006',
- type: 'http',
- status: 'up',
- },
- url: {
- scheme: 'https',
- port: 443,
- domain: 'www.google.com',
- full: 'https://www.google.com',
- },
+ monitor: getMonitorData({ id: monitorId, name, status: 'up', timestamp }),
'@timestamp': timestamp ?? '2022-12-18T09:52:04.056Z',
- ecs: {
- version: '8.0.0',
- },
config_id: configId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
- data_stream: {
- namespace: 'default',
- type: 'synthetics',
- dataset: 'http',
- },
- tls: {
- cipher: 'TLS-AES-128-GCM-SHA256',
- certificate_not_valid_before: '2022-11-28T08:19:01.000Z',
- established: true,
- server: {
- x509: {
- not_after: '2023-02-20T08:19:00.000Z',
- subject: {
- distinguished_name: 'CN=www.google.com',
- common_name: 'www.google.com',
- },
- not_before: '2022-11-28T08:19:01.000Z',
- public_key_curve: 'P-256',
- public_key_algorithm: 'ECDSA',
- signature_algorithm: 'SHA256-RSA',
- serial_number: '173037077033925240295268439311466214245',
- issuer: {
- distinguished_name: 'CN=GTS CA 1C3,O=Google Trust Services LLC,C=US',
- common_name: 'GTS CA 1C3',
- },
- },
- hash: {
- sha1: 'ea1b44061b864526c45619230b3299117d11bf4e',
- sha256: 'a5686448de09cc82b9cdad1e96357f919552ab14244da7948dd412ec0fc37d2b',
- },
- },
- rtt: {
- handshake: {
- us: 35023,
- },
- },
- version: '1.3',
- certificate_not_valid_after: '2023-02-20T08:19:00.000Z',
- version_protocol: 'tls',
- },
state: {
duration_ms: 0,
checks: 1,
@@ -135,67 +50,81 @@ export const getUpHit = ({
flap_history: [],
status: 'up',
},
- event: {
- agent_id_status: 'verified',
- ingested: '2022-12-18T09:52:11Z',
- dataset: 'http',
- },
...(testRunId && { test_run_id: testRunId }),
- http: {
- rtt: {
- response_header: {
- us: 144758,
- },
- total: {
- us: 149191,
- },
- write_request: {
- us: 48,
- },
- content: {
- us: 401,
- },
- validate: {
- us: 145160,
- },
- },
- response: {
- headers: {
- Server: 'gws',
- P3p: 'CP="This is not a P3P policy! See g.co/p3phelp for more info."',
- Date: 'Thu, 29 Dec 2022 08:17:09 GMT',
- 'X-Frame-Options': 'SAMEORIGIN',
- 'Accept-Ranges': 'none',
- 'Cache-Control': 'private, max-age=0',
- 'X-Xss-Protection': '0',
- 'Cross-Origin-Opener-Policy-Report-Only': 'same-origin-allow-popups; report-to="gws"',
- Vary: 'Accept-Encoding',
- Expires: '-1',
- 'Content-Type': 'text/html; charset=ISO-8859-1',
- },
- status_code: 200,
- mime_type: 'text/html; charset=utf-8',
- body: {
- bytes: 13963,
- hash: 'a4c2cf7dead9fb9329fc3727fc152b6a12072410926430491d02a0c6dc3a70ff',
- },
- },
- },
});
-export const firstDownHit = ({
+export const makeDownSummary = ({
name,
timestamp,
monitorId,
- locationName,
+ location,
configId,
}: DocOverrides = {}) => ({
- ...getGeoData(locationName),
+ ...getGeoData(location),
+ ...commons,
summary: {
up: 0,
down: 1,
final_attempt: true,
},
+ monitor: getMonitorData({ id: monitorId, name, status: 'down', timestamp }),
+ error: {
+ message: 'received status code 200 expecting [500]',
+ type: 'validate',
+ },
+ '@timestamp': timestamp ?? '2022-12-18T09:49:49.976Z',
+ config_id: configId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
+ state: {
+ duration_ms: 0,
+ checks: 1,
+ ends: null,
+ started_at: '2022-12-18T09:49:56.007551998Z',
+ id: 'Test private location-18524a3d9a7-0',
+ up: 0,
+ down: 1,
+ flap_history: [],
+ status: 'down',
+ },
+});
+
+const getMonitorData = ({
+ id,
+ name,
+ status,
+ timestamp,
+}: {
+ id?: string;
+ name?: string;
+ status: 'up' | 'down';
+ timestamp?: string;
+}) => ({
+ duration: {
+ us: 152459,
+ },
+ origin: 'ui',
+ ip: '142.250.181.196',
+ name: name ?? 'Test Monitor',
+ fleet_managed: true,
+ check_group: uuidv4(),
+ timespan: {
+ lt: timestamp ?? '2022-12-18T09:52:50.128Z',
+ gte: timestamp ? moment(timestamp).subtract(3, 'minutes') : '2022-12-18T09:49:50.128Z',
+ },
+ id: id ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
+ type: 'http',
+ status: status ?? 'down',
+});
+
+const commons = {
+ url: {
+ scheme: 'https',
+ port: 443,
+ domain: 'www.google.com',
+ full: 'https://www.google.com',
+ },
+ ecs: {
+ version: '8.0.0',
+ },
tcp: {
rtt: {
connect: {
@@ -203,6 +132,11 @@ export const firstDownHit = ({
},
},
},
+ event: {
+ agent_id_status: 'verified',
+ ingested: '2022-12-18T09:49:57Z',
+ dataset: 'http',
+ },
agent: {
name: 'docker-fleet-server',
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
@@ -210,54 +144,22 @@ export const firstDownHit = ({
ephemeral_id: '264bb432-93f6-4aa6-a14d-266c53b9e7c7',
version: '8.7.0',
},
- resolve: {
- rtt: {
- us: 3234,
- },
- ip: '142.250.181.196',
- },
elastic_agent: {
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
version: '8.7.0',
snapshot: true,
},
- monitor: {
- duration: {
- us: 152459,
- },
- origin: 'ui',
- ip: '142.250.181.196',
- name: name ?? 'Test Monitor',
- fleet_managed: true,
- check_group: uuidv4(),
- timespan: {
- lt: '2022-12-18T09:52:50.128Z',
- gte: '2022-12-18T09:49:50.128Z',
- },
- id: monitorId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
- type: 'http',
- status: 'down',
- },
- error: {
- message: 'received status code 200 expecting [500]',
- type: 'validate',
- },
- url: {
- scheme: 'https',
- port: 443,
- domain: 'www.google.com',
- full: 'https://www.google.com',
- },
- '@timestamp': timestamp ?? '2022-12-18T09:49:49.976Z',
- ecs: {
- version: '8.0.0',
- },
- config_id: configId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
data_stream: {
namespace: 'default',
type: 'synthetics',
dataset: 'http',
},
+ resolve: {
+ rtt: {
+ us: 3101,
+ },
+ ip: '142.250.181.196',
+ },
tls: {
established: true,
cipher: 'TLS-AES-128-GCM-SHA256',
@@ -293,22 +195,6 @@ export const firstDownHit = ({
certificate_not_valid_after: '2023-02-20T08:19:00.000Z',
version_protocol: 'tls',
},
- state: {
- duration_ms: 0,
- checks: 1,
- ends: null,
- started_at: '2022-12-18T09:49:56.007551998Z',
- id: 'Test private location-18524a3d9a7-0',
- up: 0,
- down: 1,
- flap_history: [],
- status: 'down',
- },
- event: {
- agent_id_status: 'verified',
- ingested: '2022-12-18T09:49:57Z',
- dataset: 'http',
- },
http: {
rtt: {
response_header: {
@@ -349,4 +235,4 @@ export const firstDownHit = ({
},
},
},
-});
+};
diff --git a/x-pack/packages/observability/synthetics_test_data/src/utils.ts b/x-pack/packages/observability/synthetics_test_data/src/utils.ts
new file mode 100644
index 0000000000000..cf35a93491d54
--- /dev/null
+++ b/x-pack/packages/observability/synthetics_test_data/src/utils.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const getGeoData = ({ id, label }: { label?: string; id?: string } = {}) => ({
+ observer: {
+ geo: {
+ name: label ?? 'Dev Service',
+ location: '41.8780, 93.0977',
+ },
+ name: id ?? 'dev',
+ },
+});
diff --git a/x-pack/packages/observability/synthetics_test_data/tsconfig.json b/x-pack/packages/observability/synthetics_test_data/tsconfig.json
new file mode 100644
index 0000000000000..86d57b8d692f7
--- /dev/null
+++ b/x-pack/packages/observability/synthetics_test_data/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node",
+ "react"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ ]
+}
diff --git a/x-pack/packages/security-solution/features/src/product_features_keys.ts b/x-pack/packages/security-solution/features/src/product_features_keys.ts
index 5a1a9f93d351f..6000c110d9298 100644
--- a/x-pack/packages/security-solution/features/src/product_features_keys.ts
+++ b/x-pack/packages/security-solution/features/src/product_features_keys.ts
@@ -54,10 +54,16 @@ export enum ProductFeatureSecurityKey {
osqueryAutomatedResponseActions = 'osquery_automated_response_actions',
/**
- * Enables Agent Tamper Protection
+ * Enables Protection Updates
*/
endpointProtectionUpdates = 'endpoint_protection_updates',
+ /**
+ * Enables Endpoint Custom Notification
+ */
+
+ endpointCustomNotification = 'endpoint_custom_notification',
+
/**
* Enables Agent Tamper Protection
*/
diff --git a/x-pack/packages/security-solution/features/src/security/product_feature_config.ts b/x-pack/packages/security-solution/features/src/security/product_feature_config.ts
index 2859f359f27c6..d9157b9bb5174 100644
--- a/x-pack/packages/security-solution/features/src/security/product_feature_config.ts
+++ b/x-pack/packages/security-solution/features/src/security/product_feature_config.ts
@@ -126,6 +126,7 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
[ProductFeatureSecurityKey.osqueryAutomatedResponseActions]: {},
[ProductFeatureSecurityKey.endpointProtectionUpdates]: {},
[ProductFeatureSecurityKey.endpointAgentTamperProtection]: {},
+ [ProductFeatureSecurityKey.endpointCustomNotification]: {},
[ProductFeatureSecurityKey.externalRuleActions]: {},
[ProductFeatureSecurityKey.cloudSecurityPosture]: {},
diff --git a/x-pack/packages/security-solution/upselling/service/types.ts b/x-pack/packages/security-solution/upselling/service/types.ts
index 83a6e2f6bb12a..43019271a7e02 100644
--- a/x-pack/packages/security-solution/upselling/service/types.ts
+++ b/x-pack/packages/security-solution/upselling/service/types.ts
@@ -17,6 +17,7 @@ export type UpsellingSectionId =
| 'osquery_automated_response_actions'
| 'endpoint_protection_updates'
| 'endpoint_agent_tamper_protection'
+ | 'endpoint_custom_notification'
| 'cloud_security_posture_integration_installation'
| 'ruleDetailsEndpointExceptions'
| 'integration_assistant';
diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx
index 45b263b66f2fb..6fef00ccecec9 100644
--- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx
+++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx
@@ -160,9 +160,7 @@ export class FeatureTable extends Component {
{helpText && (
<>
-
- {helpText}
-
+
>
)}
@@ -404,7 +402,7 @@ export class FeatureTable extends Component {
'xpack.security.management.editRole.featureTable.managementCategoryHelpText',
{
defaultMessage:
- 'Access to Stack Management is determined by both Elasticsearch and Kibana privileges, and cannot be explicitly disabled.',
+ 'Additional Stack Management permissions can be found outside of this menu, in index and cluster privileges.',
}
);
}
diff --git a/x-pack/plugins/actions/common/connector_feature_config.test.ts b/x-pack/plugins/actions/common/connector_feature_config.test.ts
index 1f18e923593d5..2c80203c5baf2 100644
--- a/x-pack/plugins/actions/common/connector_feature_config.test.ts
+++ b/x-pack/plugins/actions/common/connector_feature_config.test.ts
@@ -44,7 +44,7 @@ describe('getConnectorCompatibility', () => {
it('returns the compatibility list for valid feature ids', () => {
expect(
getConnectorCompatibility(['alerting', 'cases', 'uptime', 'siem', 'generativeAIForSecurity'])
- ).toEqual(['Alerting Rules', 'Cases', 'Generative AI for Security']);
+ ).toEqual(['Alerting Rules', 'Cases', 'Security Solution', 'Generative AI for Security']);
});
it('skips invalid feature ids', () => {
diff --git a/x-pack/plugins/actions/common/connector_feature_config.ts b/x-pack/plugins/actions/common/connector_feature_config.ts
index 588966b307db1..5ba316f47d59b 100644
--- a/x-pack/plugins/actions/common/connector_feature_config.ts
+++ b/x-pack/plugins/actions/common/connector_feature_config.ts
@@ -56,6 +56,12 @@ const compatibilityAlertingRules = i18n.translate(
defaultMessage: 'Alerting Rules',
}
);
+const compatibilitySecuritySolution = i18n.translate(
+ 'xpack.actions.availableConnectorFeatures.compatibility.securitySolution',
+ {
+ defaultMessage: 'Security Solution',
+ }
+);
const compatibilityCases = i18n.translate(
'xpack.actions.availableConnectorFeatures.compatibility.cases',
@@ -93,7 +99,7 @@ export const SecuritySolutionFeature: ConnectorFeatureConfig = {
name: i18n.translate('xpack.actions.availableConnectorFeatures.securitySolution', {
defaultMessage: 'Security Solution',
}),
- compatibility: compatibilityAlertingRules,
+ compatibility: compatibilitySecuritySolution,
};
export const GenerativeAIForSecurityFeature: ConnectorFeatureConfig = {
diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts
index 980144117e4e8..399f92090818b 100644
--- a/x-pack/plugins/actions/server/config.ts
+++ b/x-pack/plugins/actions/server/config.ts
@@ -145,6 +145,15 @@ export const configSchema = schema.object({
max: schema.maybe(schema.number({ min: MIN_QUEUED_MAX, defaultValue: DEFAULT_QUEUED_MAX })),
})
),
+ usage: schema.maybe(
+ schema.object({
+ cert: schema.maybe(
+ schema.object({
+ path: schema.string(),
+ })
+ ),
+ })
+ ),
});
export type ActionsConfig = TypeOf;
diff --git a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap
index 2ce82a1b3925f..478957afb1d66 100644
--- a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap
+++ b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap
@@ -9852,10 +9852,12 @@ Object {
"type": "keyword",
},
"location.id": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
"location.name": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
@@ -9867,6 +9869,10 @@ Object {
"required": false,
"type": "keyword",
},
+ "monitor.state.id": Object {
+ "required": false,
+ "type": "keyword",
+ },
"monitor.tags": Object {
"array": true,
"required": false,
@@ -9877,6 +9883,12 @@ Object {
"type": "keyword",
},
"observer.geo.name": Object {
+ "array": true,
+ "required": false,
+ "type": "keyword",
+ },
+ "observer.name": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
@@ -9972,10 +9984,12 @@ Object {
"type": "keyword",
},
"location.id": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
"location.name": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
@@ -9987,6 +10001,10 @@ Object {
"required": false,
"type": "keyword",
},
+ "monitor.state.id": Object {
+ "required": false,
+ "type": "keyword",
+ },
"monitor.tags": Object {
"array": true,
"required": false,
@@ -9997,6 +10015,12 @@ Object {
"type": "keyword",
},
"observer.geo.name": Object {
+ "array": true,
+ "required": false,
+ "type": "keyword",
+ },
+ "observer.name": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
@@ -10092,10 +10116,12 @@ Object {
"type": "keyword",
},
"location.id": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
"location.name": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
@@ -10107,6 +10133,10 @@ Object {
"required": false,
"type": "keyword",
},
+ "monitor.state.id": Object {
+ "required": false,
+ "type": "keyword",
+ },
"monitor.tags": Object {
"array": true,
"required": false,
@@ -10117,6 +10147,12 @@ Object {
"type": "keyword",
},
"observer.geo.name": Object {
+ "array": true,
+ "required": false,
+ "type": "keyword",
+ },
+ "observer.name": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
@@ -10212,10 +10248,12 @@ Object {
"type": "keyword",
},
"location.id": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
"location.name": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
@@ -10227,6 +10265,10 @@ Object {
"required": false,
"type": "keyword",
},
+ "monitor.state.id": Object {
+ "required": false,
+ "type": "keyword",
+ },
"monitor.tags": Object {
"array": true,
"required": false,
@@ -10237,6 +10279,12 @@ Object {
"type": "keyword",
},
"observer.geo.name": Object {
+ "array": true,
+ "required": false,
+ "type": "keyword",
+ },
+ "observer.name": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
@@ -10338,10 +10386,12 @@ Object {
"type": "keyword",
},
"location.id": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
"location.name": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
@@ -10353,6 +10403,10 @@ Object {
"required": false,
"type": "keyword",
},
+ "monitor.state.id": Object {
+ "required": false,
+ "type": "keyword",
+ },
"monitor.tags": Object {
"array": true,
"required": false,
@@ -10363,6 +10417,12 @@ Object {
"type": "keyword",
},
"observer.geo.name": Object {
+ "array": true,
+ "required": false,
+ "type": "keyword",
+ },
+ "observer.name": Object {
+ "array": true,
"required": false,
"type": "keyword",
},
diff --git a/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx
index 7a9d175632f27..3d883550bd4a8 100644
--- a/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx
@@ -7,20 +7,20 @@
import React, { useMemo } from 'react';
import {
- EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
EuiLoadingSpinner,
EuiPanel,
EuiSpacer,
+ EuiStat,
} from '@elastic/eui';
import prettyMilliseconds from 'pretty-ms';
import { CaseStatuses } from '../../../common/types/domain';
import { useGetCasesStatus } from '../../containers/use_get_cases_status';
import { StatusStats } from '../status/status_stats';
import { useGetCasesMetrics } from '../../containers/use_get_cases_metrics';
-import { ATTC_DESCRIPTION, ATTC_STAT } from './translations';
+import { ATTC_DESCRIPTION, ATTC_STAT, ATTC_STAT_INFO_ARIA_LABEL } from './translations';
export const CasesMetrics: React.FC = () => {
const {
@@ -68,23 +68,28 @@ export const CasesMetrics: React.FC = () => {
/>
-
- {ATTC_STAT}
- >
- ),
- description: isCasesMetricsLoading ? (
-
- ) : (
- mttrValue
- ),
- },
- ]}
+ description={
+ <>
+ {ATTC_STAT}
+
+
+ >
+ }
+ title={
+ isCasesMetricsLoading ? (
+
+ ) : (
+ mttrValue
+ )
+ }
+ titleSize="xs"
+ text-align="left"
/>
diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts
index e29019516e911..a69c942140602 100644
--- a/x-pack/plugins/cases/public/components/all_cases/translations.ts
+++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts
@@ -128,6 +128,13 @@ export const ATTC_STAT = i18n.translate('xpack.cases.casesStats.mttr', {
defaultMessage: 'Average time to close',
});
+export const ATTC_STAT_INFO_ARIA_LABEL = i18n.translate(
+ 'xpack.cases.casesStats.mttr.info.ariaLabel',
+ {
+ defaultMessage: 'More about average time to close',
+ }
+);
+
export const ATTC_DESCRIPTION = i18n.translate('xpack.cases.casesStats.mttrDescription', {
defaultMessage: 'The average duration (from creation to closure) for your current cases',
});
diff --git a/x-pack/plugins/cases/public/components/status/status_stats.test.tsx b/x-pack/plugins/cases/public/components/status/status_stats.test.tsx
index e24e3019f8b9c..d6958a9d5654a 100644
--- a/x-pack/plugins/cases/public/components/status/status_stats.test.tsx
+++ b/x-pack/plugins/cases/public/components/status/status_stats.test.tsx
@@ -27,9 +27,7 @@ describe('Stats', () => {
it('shows the count', async () => {
const wrapper = mount( );
- expect(
- wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__description`).first().text()
- ).toBe('2');
+ expect(wrapper.find(`[data-test-subj="test-stats"] .euiStat__title`).first().text()).toBe('2');
});
it('shows the loading spinner', async () => {
@@ -39,29 +37,29 @@ describe('Stats', () => {
});
describe('Status title', () => {
- it('shows the correct title for status open', async () => {
+ it('shows the correct description for status open', async () => {
const wrapper = mount( );
expect(
- wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text()
+ wrapper.find(`[data-test-subj="test-stats"] .euiStat__description`).first().text()
).toBe('Open cases');
});
- it('shows the correct title for status in-progress', async () => {
+ it('shows the correct description for status in-progress', async () => {
const wrapper = mount(
);
expect(
- wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text()
+ wrapper.find(`[data-test-subj="test-stats"] .euiStat__description`).first().text()
).toBe('In progress cases');
});
- it('shows the correct title for status closed', async () => {
+ it('shows the correct description for status closed', async () => {
const wrapper = mount( );
expect(
- wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text()
+ wrapper.find(`[data-test-subj="test-stats"] .euiStat__description`).first().text()
).toBe('Closed cases');
});
});
diff --git a/x-pack/plugins/cases/public/components/status/status_stats.tsx b/x-pack/plugins/cases/public/components/status/status_stats.tsx
index 1f1686c26a4db..8f01c1e98aba2 100644
--- a/x-pack/plugins/cases/public/components/status/status_stats.tsx
+++ b/x-pack/plugins/cases/public/components/status/status_stats.tsx
@@ -6,7 +6,7 @@
*/
import React, { memo, useMemo } from 'react';
-import { EuiDescriptionList, EuiLoadingSpinner } from '@elastic/eui';
+import { EuiStat, EuiLoadingSpinner } from '@elastic/eui';
import type { CaseStatuses } from '../../../common/types/domain';
import { statuses } from './config';
@@ -23,21 +23,25 @@ const StatusStatsComponent: React.FC = ({
isLoading,
dataTestSubj,
}) => {
- const statusStats = useMemo(
- () => [
- {
- title: statuses[caseStatus].stats.title,
- description: isLoading ? (
-
- ) : (
- caseCount ?? '-'
- ),
- },
- ],
+ const { title, description } = useMemo(
+ () => ({
+ description: statuses[caseStatus].stats.title,
+ title: isLoading ? (
+
+ ) : (
+ caseCount ?? '-'
+ ),
+ }),
[caseCount, caseStatus, dataTestSubj, isLoading]
);
return (
-
+
);
};
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx
index 75234c0495f51..6d901a76a29c3 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_grouped_findings.tsx
@@ -70,6 +70,7 @@ export interface FindingsGroupingAggregation {
export const getGroupedFindingsQuery = (query: GroupingQuery) => ({
...query,
index: CDR_MISCONFIGURATIONS_INDEX_PATTERN,
+ ignore_unavailable: true,
size: 0,
});
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts
index f8cd0238ef0bb..f6f27e15ee7a4 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts
+++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts
@@ -51,7 +51,7 @@ export const getFindingsQuery = (
sort: getMultiFieldsSort(sort),
size: MAX_FINDINGS_TO_LOAD,
aggs: getFindingsCountAggQuery(),
- ignore_unavailable: false,
+ ignore_unavailable: true,
query: {
...query,
bool: {
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_grouped_vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_grouped_vulnerabilities.tsx
index 580926340438f..9fddf97e28482 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_grouped_vulnerabilities.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_grouped_vulnerabilities.tsx
@@ -57,6 +57,7 @@ export type VulnerabilitiesRootGroupingAggregation =
export const getGroupedVulnerabilitiesQuery = (query: GroupingQuery) => ({
...query,
index: CDR_VULNERABILITIES_INDEX_PATTERN,
+ ignore_unavailable: true,
size: 0,
});
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx
index 3e5bd646e7993..0d0ea9ba5a22f 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx
@@ -57,6 +57,7 @@ export const getVulnerabilitiesQuery = (
pageParam: number
) => ({
index: CDR_VULNERABILITIES_INDEX_PATTERN,
+ ignore_unavailable: true,
sort: getMultiFieldsSort(sort),
size: MAX_FINDINGS_TO_LOAD,
query: {
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx
index 353fc84546a9c..592da5d044f2a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx
@@ -122,7 +122,7 @@ export const SearchIndexDocuments: React.FC = () => {
docs={docs}
docsPerPage={pagination.pageSize ?? 10}
isLoading={status !== Status.SUCCESS && mappingStatus !== Status.SUCCESS}
- mappings={mappingData?.mappings?.properties ?? {}}
+ mappings={mappingData ? { [indexName]: mappingData } : undefined}
meta={data?.meta ?? DEFAULT_PAGINATION}
onPaginate={(pageIndex) => setPagination({ ...pagination, pageIndex })}
setDocsPerPage={(pageSize) => setPagination({ ...pagination, pageSize })}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.test.tsx
index 9781270d9dd81..4df4b4fe912d9 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.test.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.test.tsx
@@ -109,6 +109,39 @@ describe('AgentPolicyActionMenu', () => {
const deleteButton = result.getByTestId('agentPolicyActionMenuDeleteButton');
expect(deleteButton).toHaveAttribute('disabled');
});
+
+ it('is disabled when agent policy support agentless is true', () => {
+ const testRenderer = createFleetTestRendererMock();
+ const agentlessPolicy: AgentPolicy = {
+ ...baseAgentPolicy,
+ supports_agentless: true,
+ package_policies: [
+ {
+ id: 'test-package-policy',
+ is_managed: false,
+ created_at: new Date().toISOString(),
+ created_by: 'test',
+ enabled: true,
+ inputs: [],
+ name: 'test-package-policy',
+ namespace: 'default',
+ policy_id: 'test',
+ policy_ids: ['test'],
+ revision: 1,
+ updated_at: new Date().toISOString(),
+ updated_by: 'test',
+ },
+ ],
+ };
+
+ const result = testRenderer.render( );
+
+ const agentActionsButton = result.getByTestId('agentActionsBtn');
+ agentActionsButton.click();
+
+ const deleteButton = result.getByTestId('agentPolicyActionMenuDeleteButton');
+ expect(deleteButton).not.toHaveAttribute('disabled');
+ });
});
describe('add agent', () => {
@@ -176,6 +209,39 @@ describe('AgentPolicyActionMenu', () => {
const addButton = result.getByTestId('agentPolicyActionMenuAddAgentButton');
expect(addButton).toHaveAttribute('disabled');
});
+
+ it('should remove add agent button when agent policy support agentless is true', () => {
+ const testRenderer = createFleetTestRendererMock();
+ const agentlessPolicy: AgentPolicy = {
+ ...baseAgentPolicy,
+ supports_agentless: true,
+ package_policies: [
+ {
+ id: 'test-package-policy',
+ is_managed: false,
+ created_at: new Date().toISOString(),
+ created_by: 'test',
+ enabled: true,
+ inputs: [],
+ name: 'test-package-policy',
+ namespace: 'default',
+ policy_id: 'test',
+ policy_ids: ['test'],
+ revision: 1,
+ updated_at: new Date().toISOString(),
+ updated_by: 'test',
+ },
+ ],
+ };
+
+ const result = testRenderer.render( );
+
+ const agentActionsButton = result.getByTestId('agentActionsBtn');
+ agentActionsButton.click();
+
+ const addAgentActionButton = result.queryByTestId('agentPolicyActionMenuAddAgentButton');
+ expect(addAgentActionButton).toBeNull();
+ });
});
describe('add fleet server', () => {
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx
index 48f391a4e545d..bfb364abf8a5d 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx
@@ -100,82 +100,106 @@ export const AgentPolicyActionMenu = memo<{
);
- const menuItems = agentPolicy?.is_managed
- ? [viewPolicyItem]
- : [
+ const deletePolicyItem = (
+
+ {(deleteAgentPolicyPrompt) => (
+ ) : undefined
}
- data-test-subj="agentPolicyActionMenuAddAgentButton"
- onClick={() => {
- setIsContextMenuOpen(false);
- setIsEnrollmentFlyoutOpen(true);
- }}
- key="enrollAgents"
- >
- {isFleetServerPolicy ? (
-
- ) : (
-
- )}
- ,
- viewPolicyItem,
- {
- setIsContextMenuOpen(false);
- copyAgentPolicyPrompt(agentPolicy, onCopySuccess);
+ deleteAgentPolicyPrompt(agentPolicy.id);
}}
- key="copyPolicy"
>
- ,
-
- {(deleteAgentPolicyPrompt) => (
-
- ) : undefined
- }
- icon="trash"
- onClick={() => {
- deleteAgentPolicyPrompt(agentPolicy.id);
- }}
- >
-
-
- )}
- ,
- ];
+
+ )}
+
+ );
+
+ const copyPolicyItem = (
+ {
+ setIsContextMenuOpen(false);
+ copyAgentPolicyPrompt(agentPolicy, onCopySuccess);
+ }}
+ key="copyPolicy"
+ >
+
+
+ );
+
+ const managedMenuItems = [viewPolicyItem];
+ const agentBasedMenuItems = [
+ {
+ setIsContextMenuOpen(false);
+ setIsEnrollmentFlyoutOpen(true);
+ }}
+ key="enrollAgents"
+ >
+ {isFleetServerPolicy ? (
+
+ ) : (
+
+ )}
+ ,
+ viewPolicyItem,
+ copyPolicyItem,
+ deletePolicyItem,
+ ];
+ const agentlessMenuItems = [viewPolicyItem, deletePolicyItem];
+
+ let menuItems;
+
+ if (agentPolicy?.is_managed) {
+ menuItems = managedMenuItems;
+ } else if (agentPolicy?.supports_agentless) {
+ menuItems = agentlessMenuItems;
+ } else {
+ menuItems = agentBasedMenuItems;
+ }
- if (authz.fleet.allAgents && !agentPolicy?.is_managed) {
+ if (
+ authz.fleet.allAgents &&
+ !agentPolicy?.is_managed &&
+ !agentPolicy?.supports_agentless
+ ) {
menuItems.push(
=
const licenseService = useLicense();
const [isUninstallCommandFlyoutOpen, setIsUninstallCommandFlyoutOpen] = useState(false);
const policyHasElasticDefend = useMemo(() => hasElasticDefend(agentPolicy), [agentPolicy]);
+ const isManagedorAgentlessPolicy =
+ agentPolicy.is_managed === true || agentPolicy?.supports_agentless === true;
const AgentTamperProtectionSectionContent = useMemo(
() => (
@@ -196,7 +198,12 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
);
const AgentTamperProtectionSection = useMemo(() => {
- if (agentTamperProtectionEnabled && licenseService.isPlatinum() && !agentPolicy.is_managed) {
+ if (
+ agentTamperProtectionEnabled &&
+ licenseService.isPlatinum() &&
+ !agentPolicy.is_managed &&
+ !agentPolicy.supports_agentless
+ ) {
if (AgentTamperProtectionWrapper) {
return (
@@ -214,6 +221,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
agentPolicy.is_managed,
AgentTamperProtectionWrapper,
AgentTamperProtectionSectionContent,
+ agentPolicy.supports_agentless,
]);
return (
@@ -405,7 +413,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
>
=
>
{
@@ -582,7 +590,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
isInvalid={Boolean(touchedFields.fleet_server_host_id && validation.fleet_server_host_id)}
>
=
isDisabled={disabled}
>
=
isDisabled={disabled}
>
=
isDisabled={disabled}
>
=
>
=
>
{
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx
index 58b764ed68add..32c350108bccf 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx
@@ -23,12 +23,13 @@ import {
sendGetAgents,
} from '../../../hooks';
-import type { PackagePolicy } from '../../../types';
+import type { AgentPolicy, PackagePolicy } from '../../../types';
interface Props {
children: (deleteAgentPolicy: DeleteAgentPolicy) => React.ReactElement;
hasFleetServer: boolean;
packagePolicies?: PackagePolicy[];
+ agentPolicy: AgentPolicy;
}
export type DeleteAgentPolicy = (agentPolicy: string, onSuccess?: OnSuccessCallback) => void;
@@ -39,12 +40,13 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({
children,
hasFleetServer,
packagePolicies,
+ agentPolicy,
}) => {
const { notifications } = useStartServices();
const {
agents: { enabled: isFleetEnabled },
} = useConfig();
- const [agentPolicy, setAgentPolicy] = useState();
+ const [agentPolicyId, setAgentPolicyId] = useState();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isLoadingAgentsCount, setIsLoadingAgentsCount] = useState(false);
const [agentsCount, setAgentsCount] = useState(0);
@@ -56,20 +58,20 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({
const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies();
const deleteAgentPolicyPrompt: DeleteAgentPolicy = (
- agentPolicyToDelete,
+ agentPolicyIdToDelete,
onSuccess = () => undefined
) => {
- if (!agentPolicyToDelete) {
+ if (!agentPolicyIdToDelete) {
throw new Error('No agent policy specified for deletion');
}
setIsModalOpen(true);
- setAgentPolicy(agentPolicyToDelete);
- fetchAgentsCount(agentPolicyToDelete);
+ setAgentPolicyId(agentPolicyIdToDelete);
+ fetchAgentsCount(agentPolicyIdToDelete);
onSuccessCallback.current = onSuccess;
};
const closeModal = () => {
- setAgentPolicy(undefined);
+ setAgentPolicyId(undefined);
setIsLoading(false);
setIsLoadingAgentsCount(false);
setIsModalOpen(false);
@@ -80,7 +82,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({
try {
const { data } = await deleteAgentPolicyMutation.mutateAsync({
- agentPolicyId: agentPolicy!,
+ agentPolicyId: agentPolicyId!,
});
if (data) {
@@ -91,13 +93,13 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({
})
);
if (onSuccessCallback.current) {
- onSuccessCallback.current(agentPolicy!);
+ onSuccessCallback.current(agentPolicyId!);
}
} else {
notifications.toasts.addDanger(
i18n.translate('xpack.fleet.deleteAgentPolicy.failureSingleNotificationTitle', {
defaultMessage: "Error deleting agent policy ''{id}''",
- values: { id: agentPolicy },
+ values: { id: agentPolicyId },
})
);
}
@@ -173,7 +175,9 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({
)
}
buttonColor="danger"
- confirmButtonDisabled={isLoading || isLoadingAgentsCount || !!agentsCount}
+ confirmButtonDisabled={
+ isLoading || isLoadingAgentsCount || (!agentPolicy?.supports_agentless && !!agentsCount)
+ }
>
{packagePoliciesWithMultiplePolicies && (
<>
@@ -206,13 +210,23 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({
}
)}
>
-
+ {agentPolicy?.supports_agentless ? (
+ {agentPolicy.name},
+ }}
+ />
+ ) : (
+
+ )}
) : hasFleetServer ? (
= ({
>
) : null}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/agent_policy_options.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/agent_policy_options.tsx
index c39466d779548..29722bbc42850 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/agent_policy_options.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/agent_policy_options.tsx
@@ -127,34 +127,36 @@ export function useAgentPoliciesOptions(packageInfo?: PackageInfo) {
const agentPolicyMultiOptions: Array> = useMemo(
() =>
packageInfo && !isOutputLoading && !isAgentPoliciesLoading && !isLoadingPackagePolicies
- ? agentPolicies.map((policy) => {
- const isLimitedPackageAlreadyInPolicy =
- isPackageLimited(packageInfo!) &&
- packagePoliciesForThisPackageByAgentPolicyId?.[policy.id];
-
- const isAPMPackageAndDataOutputIsLogstash =
- packageInfo?.name === FLEET_APM_PACKAGE &&
- getDataOutputForPolicy(policy)?.type === outputType.Logstash;
-
- return {
- append: isAPMPackageAndDataOutputIsLogstash ? (
-
- }
- >
-
-
- ) : null,
- key: policy.id,
- label: policy.name,
- disabled: isLimitedPackageAlreadyInPolicy || isAPMPackageAndDataOutputIsLogstash,
- 'data-test-subj': 'agentPolicyMultiItem',
- };
- })
+ ? agentPolicies
+ .filter((policy) => policy.supports_agentless !== true)
+ .map((policy) => {
+ const isLimitedPackageAlreadyInPolicy =
+ isPackageLimited(packageInfo!) &&
+ packagePoliciesForThisPackageByAgentPolicyId?.[policy.id];
+
+ const isAPMPackageAndDataOutputIsLogstash =
+ packageInfo?.name === FLEET_APM_PACKAGE &&
+ getDataOutputForPolicy(policy)?.type === outputType.Logstash;
+
+ return {
+ append: isAPMPackageAndDataOutputIsLogstash ? (
+
+ }
+ >
+
+
+ ) : null,
+ key: policy.id,
+ label: policy.name,
+ disabled: isLimitedPackageAlreadyInPolicy || isAPMPackageAndDataOutputIsLogstash,
+ 'data-test-subj': 'agentPolicyMultiItem',
+ };
+ })
: [],
[
packageInfo,
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx
index ca96066facba3..2bae962f48e7c 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx
@@ -315,7 +315,11 @@ export function useOnSubmit({
if (
(agentCount !== 0 ||
(agentPolicies.length === 0 && selectedPolicyTab !== SelectedPolicyTab.NEW)) &&
- !(isAgentlessIntegration(packageInfo) || isAgentlessPackagePolicy(packagePolicy)) &&
+ !(
+ isAgentlessIntegration(packageInfo) ||
+ isAgentlessPackagePolicy(packagePolicy) ||
+ isAgentlessAgentPolicy(overrideCreatedAgentPolicy)
+ ) &&
formState !== 'CONFIRM'
) {
setFormState('CONFIRM');
@@ -339,10 +343,18 @@ export function useOnSubmit({
}
} catch (e) {
setFormState('VALID');
+ const agentlessPolicy = agentPolicies.find(
+ (policy) => policy?.supports_agentless === true
+ );
+
notifications.toasts.addError(e, {
- title: i18n.translate('xpack.fleet.createAgentPolicy.errorNotificationTitle', {
- defaultMessage: 'Unable to create agent policy',
- }),
+ title: agentlessPolicy?.supports_agentless
+ ? i18n.translate('xpack.fleet.createAgentlessPolicy.errorNotificationTitle', {
+ defaultMessage: 'Unable to create integration',
+ })
+ : i18n.translate('xpack.fleet.createAgentPolicy.errorNotificationTitle', {
+ defaultMessage: 'Unable to create agent policy',
+ }),
});
return;
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts
index 141622410076d..fb6aefcf7583e 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts
@@ -174,7 +174,6 @@ export function useSetupTechnology({
setNewAgentPolicy({
...newAgentBasedPolicy.current,
supports_agentless: false,
- is_managed: false,
});
setSelectedPolicyTab(SelectedPolicyTab.NEW);
updateAgentPolicies([newAgentBasedPolicy.current] as AgentPolicy[]);
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx
index 5dd391450b0cb..6603698a80186 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx
@@ -106,7 +106,7 @@ export const HeaderRightContent: React.FunctionComponent {
},
});
(sendGetOneAgentPolicy as MockFn).mockResolvedValue({
- data: { item: { id: 'agentless', name: 'Agentless policy', namespace: 'default' } },
+ data: {
+ item: {
+ id: 'agentless',
+ name: 'Agentless policy',
+ namespace: 'default',
+ supports_agentless: true,
+ },
+ },
});
render();
@@ -514,6 +521,20 @@ describe('edit package policy page', () => {
expect(sendUpdatePackagePolicy).toHaveBeenCalled();
});
+ it('should hide the multiselect agent policies when agent policy is agentless', async () => {
+ (useGetAgentPolicies as MockFn).mockReturnValue({
+ data: {
+ items: [{ id: 'agent-policy-1', name: 'Agent policy 1', supports_agentless: true }],
+ },
+ isLoading: false,
+ });
+
+ await act(async () => {
+ render();
+ });
+ expect(renderResult.queryByTestId('agentPolicyMultiSelect')).not.toBeInTheDocument();
+ });
+
describe('modify agent policies', () => {
beforeEach(() => {
useMultipleAgentPoliciesMock.mockReturnValue({ canUseMultipleAgentPolicies: true });
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx
index 2bfdd40a9df2f..e448d1376b2fe 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx
@@ -46,8 +46,6 @@ import {
StepConfigurePackagePolicy,
StepDefinePackagePolicy,
} from '../create_package_policy_page/components';
-
-import { AGENTLESS_POLICY_ID } from '../../../../../../common/constants';
import type { AgentPolicy, PackagePolicyEditExtensionComponentProps } from '../../../types';
import { pkgKeyFromPackageInfo } from '../../../services';
@@ -75,7 +73,6 @@ export const EditPackagePolicyPage = memo(() => {
} = useRouteMatch<{ policyId: string; packagePolicyId: string }>();
const packagePolicy = useGetOnePackagePolicy(packagePolicyId);
-
const extensionView = useUIExtension(
packagePolicy.data?.item?.package?.name ?? '',
'package-policy-edit'
@@ -106,8 +103,7 @@ export const EditPackagePolicyForm = memo<{
} = useConfig();
const { getHref } = useLink();
const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies();
- const { isAgentlessPackagePolicy } = useAgentless();
-
+ const { isAgentlessAgentPolicy } = useAgentless();
const {
// data
agentPolicies: existingAgentPolicies,
@@ -130,7 +126,14 @@ export const EditPackagePolicyForm = memo<{
} = usePackagePolicyWithRelatedData(packagePolicyId, {
forceUpgrade,
});
- const hasAgentlessAgentPolicy = packagePolicy.policy_ids.includes(AGENTLESS_POLICY_ID);
+
+ const hasAgentlessAgentPolicy = useMemo(
+ () =>
+ existingAgentPolicies.length === 1
+ ? existingAgentPolicies.some((policy) => isAgentlessAgentPolicy(policy))
+ : false,
+ [existingAgentPolicies, isAgentlessAgentPolicy]
+ );
const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies;
useSetIsReadOnly(!canWriteIntegrationPolicies);
@@ -451,7 +454,7 @@ export const EditPackagePolicyForm = memo<{
onChange={handleExtensionViewOnChange}
validationResults={validationResults}
isEditPage={true}
- isAgentlessEnabled={isAgentlessPackagePolicy(packagePolicy)}
+ isAgentlessEnabled={hasAgentlessAgentPolicy}
/>
);
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx
index feefebf4fa2d6..231b10782eca7 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx
@@ -71,7 +71,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
onClick={() => {
setIsReassignFlyoutOpen(true);
}}
- disabled={!agent.active && !agentPolicy}
+ disabled={(!agent.active && !agentPolicy) || agentPolicy?.supports_agentless === true}
key="reassignPolicy"
>
{
onReassignClick();
}}
- disabled={!agent.active}
+ disabled={!agent.active || agentPolicy?.supports_agentless === true}
key="reassignPolicy"
>
{
onUpgradeClick();
}}
@@ -138,7 +138,12 @@ export const TableRowActions: React.FunctionComponent<{
);
}
- if (authz.fleet.allAgents && agentTamperProtectionEnabled && agent.policy_id) {
+ if (
+ authz.fleet.allAgents &&
+ agentTamperProtectionEnabled &&
+ agent.policy_id &&
+ !agentPolicy?.supports_agentless
+ ) {
menuItems.push(
{
{selectedPolicyId && (
{(deleteAgentPolicyPrompt) => {
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx
index e7f951f9c4270..0aea38990f06b 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx
@@ -33,7 +33,7 @@ const AgentPolicyFormRow = styled(EuiFormRow)`
`;
type Props = {
- agentPolicies: Array>;
+ agentPolicies: Array>;
selectedPolicyId?: string;
setSelectedPolicyId: (agentPolicyId?: string) => void;
excludeFleetServer?: boolean;
@@ -115,10 +115,12 @@ export const AgentPolicySelection: React.FC = (props) => {
({
- value: agentPolicy.id,
- text: agentPolicy.name,
- }))}
+ options={agentPolicies
+ .filter((policy) => !policy?.supports_agentless)
+ .map((agentPolicy) => ({
+ value: agentPolicy.id,
+ text: agentPolicy.name,
+ }))}
value={selectedPolicyId}
onChange={onChangeCallback}
aria-label={i18n.translate(
diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx
index 90e680c2ff845..dbf3969ffc226 100644
--- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx
+++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx
@@ -7,12 +7,12 @@
import React from 'react';
-import { act, fireEvent } from '@testing-library/react';
+import { act } from '@testing-library/react';
import type { AgentPolicy, InMemoryPackagePolicy } from '../types';
import { createIntegrationsTestRendererMock } from '../mock';
-import { useMultipleAgentPolicies, useStartServices, useLink } from '../hooks';
+import { useMultipleAgentPolicies, useLink } from '../hooks';
import { PackagePolicyActionsMenu } from './package_policy_actions_menu';
@@ -135,6 +135,17 @@ describe('PackagePolicyActionsMenu', () => {
});
});
+ it('Should not enable upgrade button if package has upgrade and agentless policy is enabled', async () => {
+ const agentPolicies = createMockAgentPolicies({ supports_agentless: true });
+ const packagePolicy = createMockPackagePolicy({ hasUpgrade: true });
+ const { utils } = renderMenu({ agentPolicies, packagePolicy });
+
+ await act(async () => {
+ const upgradeButton = utils.getByTestId('PackagePolicyActionsUpgradeItem');
+ expect(upgradeButton).toBeDisabled();
+ });
+ });
+
it('Should not be able to delete integration from a managed policy', async () => {
const agentPolicies = createMockAgentPolicies({ is_managed: true });
const packagePolicy = createMockPackagePolicy();
@@ -154,7 +165,7 @@ describe('PackagePolicyActionsMenu', () => {
});
it('Should be able to delete integration from a managed agentless policy', async () => {
- const agentPolicies = createMockAgentPolicies({ is_managed: true, supports_agentless: true });
+ const agentPolicies = createMockAgentPolicies({ is_managed: false, supports_agentless: true });
const packagePolicy = createMockPackagePolicy();
const { utils } = renderMenu({ agentPolicies, packagePolicy });
await act(async () => {
@@ -162,23 +173,6 @@ describe('PackagePolicyActionsMenu', () => {
});
});
- it('Should navigate on delete integration when having an agentless policy', async () => {
- const agentPolicies = createMockAgentPolicies({ is_managed: true, supports_agentless: true });
- const packagePolicy = createMockPackagePolicy();
- const { utils } = renderMenu({ agentPolicies, packagePolicy });
-
- await act(async () => {
- fireEvent.click(utils.getByTestId('PackagePolicyActionsDeleteItem'));
- });
- await act(async () => {
- fireEvent.click(utils.getByTestId('confirmModalConfirmButton'));
- });
- expect(useStartServices().application.navigateToApp as jest.Mock).toHaveBeenCalledWith(
- 'fleet',
- { path: '/policies' }
- );
- });
-
it('Should show add button if the policy is not managed and showAddAgent=true', async () => {
const agentPolicies = createMockAgentPolicies();
const packagePolicy = createMockPackagePolicy({ hasUpgrade: true });
@@ -197,6 +191,15 @@ describe('PackagePolicyActionsMenu', () => {
});
});
+ it('Should not show add button if the policy is agentless and showAddAgent=true', async () => {
+ const agentPolicies = createMockAgentPolicies({ supports_agentless: true });
+ const packagePolicy = createMockPackagePolicy({ hasUpgrade: true });
+ const { utils } = renderMenu({ agentPolicies, packagePolicy, showAddAgent: true });
+ await act(async () => {
+ expect(utils.queryByText('Add agent')).toBeNull();
+ });
+ });
+
it('Should show Edit integration with correct href when agentPolicy is defined', async () => {
const agentPolicies = createMockAgentPolicies();
const packagePolicy = createMockPackagePolicy();
diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx
index fcebfcb2f2475..4da1711b28313 100644
--- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx
+++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx
@@ -10,11 +10,9 @@ import { EuiContextMenuItem, EuiPortal } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { AgentPolicy, InMemoryPackagePolicy } from '../types';
-import { useAgentPolicyRefresh, useAuthz, useLink, useStartServices } from '../hooks';
+import { useAgentPolicyRefresh, useAuthz, useLink } from '../hooks';
import { policyHasFleetServer } from '../services';
-import { PLUGIN_ID, pagePathGetters } from '../constants';
-
import { AgentEnrollmentFlyout } from './agent_enrollment_flyout';
import { ContextMenuActions } from './context_menu_actions';
import { DangerEuiContextMenuItem } from './danger_eui_context_menu_item';
@@ -38,9 +36,6 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false);
const { getHref } = useLink();
const authz = useAuthz();
- const {
- application: { navigateToApp },
- } = useStartServices();
const agentPolicy = agentPolicies.length > 0 ? agentPolicies[0] : undefined; // TODO: handle multiple agent policies
const canWriteIntegrationPolicies = authz.integrations.writeIntegrationPolicies;
@@ -54,7 +49,8 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
const agentPolicyIsManaged = Boolean(agentPolicy?.is_managed);
const isOrphanedPolicy = !agentPolicy && packagePolicy.policy_ids.length === 0;
- const isAddAgentVisible = showAddAgent && agentPolicy && !agentPolicyIsManaged;
+ const isAddAgentVisible =
+ showAddAgent && agentPolicy && !agentPolicyIsManaged && !agentPolicy?.supports_agentless;
const onEnrollmentFlyoutClose = useMemo(() => {
return () => setIsEnrollmentFlyoutOpen(false);
@@ -115,7 +111,10 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
{
deletePackagePoliciesPrompt([packagePolicy.id], () => {
setIsActionsMenuOpen(false);
- if (agentPolicy?.supports_agentless) {
- // go back to all agent policies
- navigateToApp(PLUGIN_ID, { path: pagePathGetters.policies_list()[1] });
- } else {
- refreshAgentPolicy();
- }
+ refreshAgentPolicy();
});
}}
>
diff --git a/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx
index 6369d344a2d9f..7d71915fda252 100644
--- a/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx
+++ b/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx
@@ -10,7 +10,12 @@ import { EuiCallOut, EuiConfirmModal, EuiSpacer, EuiIconTip } from '@elastic/eui
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
-import { useStartServices, sendDeletePackagePolicy, useConfig } from '../hooks';
+import {
+ useStartServices,
+ sendDeletePackagePolicy,
+ sendDeleteAgentPolicy,
+ useConfig,
+} from '../hooks';
import { AGENTS_PREFIX } from '../../common/constants';
import type { AgentPolicy } from '../types';
import { sendGetAgents, useMultipleAgentPolicies } from '../hooks';
@@ -126,6 +131,26 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({
defaultMessage: "Deleted integration ''{id}''",
values: { id: successfulResults[0].name || successfulResults[0].id },
});
+
+ const agentlessPolicy = agentPolicies?.find(
+ (policy) => policy.supports_agentless === true
+ );
+
+ if (!!agentlessPolicy) {
+ try {
+ await sendDeleteAgentPolicy({ agentPolicyId: agentlessPolicy.id });
+ } catch (e) {
+ notifications.toasts.addDanger(
+ i18n.translate(
+ 'xpack.fleet.deletePackagePolicy.fatalErrorAgentlessNotificationTitle',
+ {
+ defaultMessage: 'Error deleting agentless deployment',
+ }
+ )
+ );
+ }
+ }
+
notifications.toasts.addSuccess(successMessage);
}
@@ -155,10 +180,14 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({
}
closeModal();
},
- [closeModal, packagePolicies, notifications.toasts]
+ [closeModal, packagePolicies, notifications.toasts, agentPolicies]
);
const renderModal = () => {
+ const isAgentlessPolicy = agentPolicies?.find((policy) => policy?.supports_agentless === true);
+ const packagePolicy = agentPolicies?.[0]?.package_policies?.find(
+ (policy) => policy.id === packagePolicies[0]
+ );
if (!isModalOpen) {
return null;
}
@@ -166,11 +195,18 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({
return (
+ isAgentlessPolicy ? (
+
+ ) : (
+
+ )
}
onCancel={closeModal}
onConfirm={deletePackagePolicies}
@@ -224,14 +260,16 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({
+ !isAgentlessPolicy && (
+
+ )
}
>
- {hasMultipleAgentPolicies ? (
+ {hasMultipleAgentPolicies && !isAgentlessPolicy && (
= ({
),
}}
/>
- ) : (
+ )}{' '}
+ {!hasMultipleAgentPolicies && !isAgentlessPolicy && (
= ({
}}
/>
)}
+ {!hasMultipleAgentPolicies && isAgentlessPolicy && (
+ {packagePolicy?.name},
+ }}
+ />
+ )}
>
) : null}
+
{!isLoadingAgentsCount && (
{
+ return sendRequest({
+ path: agentPolicyRouteService.getDeletePath(),
+ method: 'post',
+ body: JSON.stringify(body),
+ version: API_VERSIONS.public.v1,
+ });
+};
+
export function useDeleteAgentPolicyMutation() {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: function sendDeleteAgentPolicy(body: DeleteAgentPolicyRequest['body']) {
- return sendRequest({
- path: agentPolicyRouteService.getDeletePath(),
- method: 'post',
- body: JSON.stringify(body),
- version: API_VERSIONS.public.v1,
- });
- },
+ mutationFn: sendDeleteAgentPolicy,
onSuccess: () => {
return queryClient.invalidateQueries(['agentPolicies']);
},
diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts
index dfd92951c8919..d8971948397d3 100644
--- a/x-pack/plugins/fleet/server/errors/handlers.ts
+++ b/x-pack/plugins/fleet/server/errors/handlers.ts
@@ -44,6 +44,7 @@ import {
FleetNotFoundError,
PackageSavedObjectConflictError,
FleetTooManyRequestsError,
+ AgentlessPolicyExistsRequestError,
} from '.';
type IngestErrorHandler = (
@@ -111,6 +112,9 @@ const getHTTPResponseCode = (error: FleetError): number => {
if (error instanceof PackageAlreadyInstalledError) {
return 409;
}
+ if (error instanceof AgentlessPolicyExistsRequestError) {
+ return 409;
+ }
// Unsupported Media Type
if (error instanceof PackageUnsupportedMediaTypeError) {
return 415;
diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts
index 80d8116baaaa3..09b387e7a5cee 100644
--- a/x-pack/plugins/fleet/server/errors/index.ts
+++ b/x-pack/plugins/fleet/server/errors/index.ts
@@ -57,6 +57,12 @@ export class AgentlessAgentCreateError extends FleetError {
}
}
+export class AgentlessPolicyExistsRequestError extends AgentPolicyError {
+ constructor(message: string) {
+ super(`Unable to create integration. ${message}`);
+ }
+}
+
export class AgentPolicyNameExistsError extends AgentPolicyError {}
export class AgentReassignmentError extends FleetError {}
export class PackagePolicyIneligibleForUpgradeError extends FleetError {}
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts
index 9f42746c9c5fa..00bc01aa1f2cb 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts
@@ -363,7 +363,7 @@ describe('Agent policy', () => {
);
});
- it('should create a policy with is_managed true if agentless feature flag is set and in serverless env', async () => {
+ it('should create a policy agentless feature flag is set and in serverless env', async () => {
jest
.spyOn(appContextService, 'getExperimentalFeatures')
.mockReturnValue({ agentless: true } as any);
@@ -392,7 +392,7 @@ describe('Agent policy', () => {
namespace: 'default',
supports_agentless: true,
status: 'active',
- is_managed: true,
+ is_managed: false,
revision: 1,
updated_at: expect.anything(),
updated_by: 'system',
@@ -401,7 +401,7 @@ describe('Agent policy', () => {
});
});
- it('should create a policy with is_managed true if agentless feature flag is set and in cloud env', async () => {
+ it('should create a policy if agentless feature flag is set and in cloud env', async () => {
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
jest.spyOn(appContextService, 'getConfig').mockReturnValue({
agentless: { enabled: true },
@@ -428,7 +428,7 @@ describe('Agent policy', () => {
namespace: 'default',
supports_agentless: true,
status: 'active',
- is_managed: true,
+ is_managed: false,
revision: 1,
updated_at: expect.anything(),
updated_by: 'system',
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index 57514ec30052b..ffdb2c4162d52 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.ts
@@ -84,6 +84,7 @@ import {
FleetUnauthorizedError,
HostedAgentPolicyRestrictionRelatedError,
PackagePolicyRestrictionRelatedError,
+ AgentlessPolicyExistsRequestError,
} from '../errors';
import type { FullAgentConfigMap } from '../../common/types/models/agent_cm';
@@ -113,6 +114,7 @@ import { createSoFindIterable } from './utils/create_so_find_iterable';
import { isAgentlessEnabled } from './utils/agentless';
import { validatePolicyNamespaceForSpace } from './spaces/policy_namespaces';
import { isSpaceAwarenessEnabled } from './spaces/helpers';
+import { agentlessAgentService } from './agents/agentless_agent';
import { scheduleDeployAgentPoliciesTask } from './agent_policies/deploy_agent_policies_task';
const KEY_EDITABLE_FOR_MANAGED_POLICIES = ['namespace'];
@@ -387,7 +389,7 @@ class AgentPolicyService {
{
...agentPolicy,
status: 'active',
- is_managed: (agentPolicy.is_managed || agentPolicy?.supports_agentless) ?? false,
+ is_managed: agentPolicy.is_managed ?? false,
revision: 1,
updated_at: new Date().toISOString(),
updated_by: options?.user?.username || 'system',
@@ -411,7 +413,7 @@ class AgentPolicyService {
public async requireUniqueName(
soClient: SavedObjectsClientContract,
- givenPolicy: { id?: string; name: string }
+ givenPolicy: { id?: string; name: string; supports_agentless?: boolean | null }
) {
const savedObjectType = await getAgentPolicySavedObjectType();
@@ -423,7 +425,11 @@ class AgentPolicyService {
const idsWithName = results.total && results.saved_objects.map(({ id }) => id);
if (Array.isArray(idsWithName)) {
const isEditingSelf = givenPolicy.id && idsWithName.includes(givenPolicy.id);
- if (!givenPolicy.id || !isEditingSelf) {
+
+ if (
+ (!givenPolicy?.supports_agentless && !givenPolicy.id) ||
+ (!givenPolicy?.supports_agentless && !isEditingSelf)
+ ) {
const isSinglePolicy = idsWithName.length === 1;
const existClause = isSinglePolicy
? `Agent Policy '${idsWithName[0]}' already exists`
@@ -431,6 +437,13 @@ class AgentPolicyService {
throw new AgentPolicyNameExistsError(`${existClause} with name '${givenPolicy.name}'`);
}
+
+ if (givenPolicy?.supports_agentless && !givenPolicy.id) {
+ const integrationName = givenPolicy.name.split(' ').pop();
+ throw new AgentlessPolicyExistsRequestError(
+ `${givenPolicy.name} already exist. Please rename the integration name ${integrationName}.`
+ );
+ }
}
}
@@ -661,6 +674,7 @@ class AgentPolicyService {
await this.requireUniqueName(soClient, {
id,
name: agentPolicy.name,
+ supports_agentless: agentPolicy?.supports_agentless,
});
}
if (agentPolicy.namespace) {
@@ -1141,6 +1155,7 @@ class AgentPolicyService {
if (agentPolicy.is_managed && !options?.force) {
throw new HostedAgentPolicyRestrictionRelatedError(`Cannot delete hosted agent policy ${id}`);
}
+
// Prevent deleting policy when assigned agents are inactive
const { total } = await getAgentsByKuery(esClient, soClient, {
showInactive: true,
@@ -1149,12 +1164,32 @@ class AgentPolicyService {
kuery: `${AGENTS_PREFIX}.policy_id:${id}`,
});
- if (total > 0) {
+ if (total > 0 && !agentPolicy?.supports_agentless) {
throw new FleetError(
'Cannot delete an agent policy that is assigned to any active or inactive agents'
);
}
+ if (agentPolicy?.supports_agentless) {
+ logger.debug(`Starting unenrolling agent from agentless policy ${id}`);
+ // unenroll offline agents for agentless policies first to avoid 404 Save Object error
+ await this.triggerAgentPolicyUpdatedEvent(esClient, 'deleted', id, {
+ spaceId: soClient.getCurrentNamespace(),
+ });
+ try {
+ // Deleting agentless deployment
+ await agentlessAgentService.deleteAgentlessAgent(id);
+ logger.debug(
+ `[Agentless API] Successfully deleted agentless deployment for single agent policy id ${id}`
+ );
+ } catch (error) {
+ logger.error(
+ `[Agentless API] Error deleting agentless deployment for single agent policy id ${id}`
+ );
+ logger.error(error);
+ }
+ }
+
const packagePolicies = await packagePolicyService.findAllForAgentPolicy(soClient, id);
if (packagePolicies.length) {
@@ -1216,9 +1251,11 @@ class AgentPolicyService {
await soClient.delete(savedObjectType, id, {
force: true, // need to delete through multiple space
});
- await this.triggerAgentPolicyUpdatedEvent(esClient, 'deleted', id, {
- spaceId: soClient.getCurrentNamespace(),
- });
+ if (!agentPolicy?.supports_agentless) {
+ await this.triggerAgentPolicyUpdatedEvent(esClient, 'deleted', id, {
+ spaceId: soClient.getCurrentNamespace(),
+ });
+ }
// cleanup .fleet-policies docs on delete
await this.deleteFleetServerPoliciesForPolicyId(esClient, id);
diff --git a/x-pack/plugins/fleet/server/services/agent_policy_create.ts b/x-pack/plugins/fleet/server/services/agent_policy_create.ts
index 4d22820b9aa1c..f370867fc493b 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy_create.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy_create.ts
@@ -21,12 +21,11 @@ import {
import type { AgentPolicy, NewAgentPolicy } from '../types';
-import { agentlessAgentService } from './agents/agentless_agent';
-
import { agentPolicyService, packagePolicyService } from '.';
import { incrementPackageName } from './package_policies';
import { bulkInstallPackages } from './epm/packages';
import { ensureDefaultEnrollmentAPIKeyForAgentPolicy } from './api_keys';
+import { agentlessAgentService } from './agents/agentless_agent';
const FLEET_SERVER_POLICY_ID = 'fleet-server-policy';
@@ -84,7 +83,7 @@ async function createPackagePolicy(
user: options.user,
bumpRevision: false,
authorizationHeader: options.authorizationHeader,
- force: options.force || agentPolicy.supports_agentless === true,
+ force: options.force,
});
}
diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts
index c98a5b63e0356..3bf21c3bec0d1 100644
--- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts
+++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts
@@ -24,7 +24,12 @@ import { appContextService } from '../app_context';
import { listEnrollmentApiKeys } from '../api_keys';
import { listFleetServerHosts } from '../fleet_server_host';
-import { prependAgentlessApiBasePathToEndpoint, isAgentlessApiEnabled } from '../utils/agentless';
+import type { AgentlessConfig } from '../utils/agentless';
+import {
+ prependAgentlessApiBasePathToEndpoint,
+ isAgentlessApiEnabled,
+ getDeletionEndpointPath,
+} from '../utils/agentless';
class AgentlessAgentService {
public async createAgentlessAgent(
@@ -42,23 +47,22 @@ class AgentlessAgentService {
};
const logger = appContextService.getLogger();
- logger.debug(`Creating agentless agent ${agentlessAgentPolicy.id}`);
+ logger.debug(`[Agentless API] Creating agentless agent ${agentlessAgentPolicy.id}`);
if (!isAgentlessApiEnabled) {
logger.error(
- 'Creating agentless agent not supported in non-cloud or non-serverless environments',
- errorMetadata
+ '[Agentless API] Creating agentless agent not supported in non-cloud or non-serverless environments'
);
throw new AgentlessAgentCreateError('Agentless agent not supported');
}
if (!agentlessAgentPolicy.supports_agentless) {
- logger.error('Agentless agent policy does not have agentless enabled');
+ logger.error('[Agentless API] Agentless agent policy does not have agentless enabled');
throw new AgentlessAgentCreateError('Agentless agent policy does not have agentless enabled');
}
const agentlessConfig = appContextService.getConfig()?.agentless;
if (!agentlessConfig) {
- logger.error('Missing agentless configuration', errorMetadata);
+ logger.error('[Agentless API] Missing agentless configuration', errorMetadata);
throw new AgentlessAgentCreateError('missing agentless configuration');
}
@@ -70,24 +74,16 @@ class AgentlessAgentService {
);
logger.debug(
- `Creating agentless agent with fleet_url: ${fleetUrl} and fleet_token: [REDACTED]`
+ `[Agentless API] Creating agentless agent with fleetUrl ${fleetUrl} and fleet_token: [REDACTED]`
);
logger.debug(
- `Creating agentless agent with TLS cert: ${
+ `[Agentless API] Creating agentless agent with TLS cert: ${
agentlessConfig?.api?.tls?.certificate ? '[REDACTED]' : 'undefined'
} and TLS key: ${agentlessConfig?.api?.tls?.key ? '[REDACTED]' : 'undefined'}
and TLS ca: ${agentlessConfig?.api?.tls?.ca ? '[REDACTED]' : 'undefined'}`
);
-
- const tlsConfig = new SslConfig(
- sslSchema.validate({
- enabled: true,
- certificate: agentlessConfig?.api?.tls?.certificate,
- key: agentlessConfig?.api?.tls?.key,
- certificateAuthorities: agentlessConfig?.api?.tls?.ca,
- })
- );
+ const tlsConfig = this.createTlsConfig(agentlessConfig);
const requestConfig: AxiosRequestConfig = {
url: prependAgentlessApiBasePathToEndpoint(agentlessConfig, '/deployments'),
@@ -114,33 +110,17 @@ class AgentlessAgentService {
requestConfig.data.stack_version = appContextService.getKibanaVersion();
}
- const requestConfigDebug = {
- ...requestConfig,
- data: {
- ...requestConfig.data,
- fleet_token: '[REDACTED]',
- },
- httpsAgent: {
- ...requestConfig.httpsAgent,
- options: {
- ...requestConfig.httpsAgent.options,
- cert: requestConfig.httpsAgent.options.cert ? '[REDACTED]' : undefined,
- key: requestConfig.httpsAgent.options.key ? '[REDACTED]' : undefined,
- ca: requestConfig.httpsAgent.options.ca ? '[REDACTED]' : undefined,
- },
- },
- };
-
- const requestConfigDebugToString = JSON.stringify(requestConfigDebug);
-
- logger.debug(`Creating agentless agent with request config ${requestConfigDebugToString}`);
+ const requestConfigDebugStatus = this.createRequestConfigDebug(requestConfig);
+ logger.debug(
+ `[Agentless API] Creating agentless agent with request config ${requestConfigDebugStatus}`
+ );
const errorMetadataWithRequestConfig: LogMeta = {
...errorMetadata,
http: {
request: {
id: traceId,
- body: requestConfigDebug.data,
+ body: requestConfig.data,
},
},
};
@@ -149,7 +129,7 @@ class AgentlessAgentService {
(error: Error | AxiosError) => {
if (!axios.isAxiosError(error)) {
logger.error(
- `Creating agentless failed with an error ${error} ${requestConfigDebugToString}`,
+ `[Agentless API] Creating agentless failed with an error ${error} ${requestConfigDebugStatus}`,
errorMetadataWithRequestConfig
);
throw new AgentlessAgentCreateError(withRequestIdMessage(error.message));
@@ -160,9 +140,9 @@ class AgentlessAgentService {
if (error.response) {
// The request was made and the server responded with a status code and error data
logger.error(
- `Creating agentless failed because the Agentless API responding with a status code that falls out of the range of 2xx: ${JSON.stringify(
+ `[Agentless API] Creating agentless failed because the Agentless API responding with a status code that falls out of the range of 2xx: ${JSON.stringify(
error.response.status
- )}} ${JSON.stringify(error.response.data)}} ${requestConfigDebugToString}`,
+ )}} ${JSON.stringify(error.response.data)}} ${requestConfigDebugStatus}`,
{
...errorMetadataWithRequestConfig,
http: {
@@ -180,7 +160,7 @@ class AgentlessAgentService {
} else if (error.request) {
// The request was made but no response was received
logger.error(
- `Creating agentless agent failed while sending the request to the Agentless API: ${errorLogCodeCause} ${requestConfigDebugToString}`,
+ `[Agentless API] Creating agentless agent failed while sending the request to the Agentless API: ${errorLogCodeCause} ${requestConfigDebugStatus}`,
errorMetadataWithRequestConfig
);
throw new AgentlessAgentCreateError(
@@ -189,7 +169,7 @@ class AgentlessAgentService {
} else {
// Something happened in setting up the request that triggered an Error
logger.error(
- `Creating agentless agent failed to be created ${errorLogCodeCause} ${requestConfigDebugToString}`,
+ `[Agentless API] Creating agentless agent failed to be created ${errorLogCodeCause} ${requestConfigDebugStatus}`,
errorMetadataWithRequestConfig
);
throw new AgentlessAgentCreateError(
@@ -199,10 +179,110 @@ class AgentlessAgentService {
}
);
- logger.debug(`Created an agentless agent ${response}`);
+ logger.debug(`[Agentless API] Created an agentless agent ${response}`);
+ return response;
+ }
+
+ public async deleteAgentlessAgent(agentlessPolicyId: string) {
+ const logger = appContextService.getLogger();
+ const agentlessConfig = appContextService.getConfig()?.agentless;
+ const tlsConfig = this.createTlsConfig(agentlessConfig);
+ const requestConfig = {
+ url: getDeletionEndpointPath(agentlessConfig, `/deployments/${agentlessPolicyId}`),
+ method: 'DELETE',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ httpsAgent: new https.Agent({
+ rejectUnauthorized: tlsConfig.rejectUnauthorized,
+ cert: tlsConfig.certificate,
+ key: tlsConfig.key,
+ ca: tlsConfig.certificateAuthorities,
+ }),
+ };
+
+ const requestConfigDebugStatus = this.createRequestConfigDebug(requestConfig);
+
+ logger.debug(
+ `[Agentless API] Start deleting agentless agent for agent policy ${requestConfigDebugStatus}`
+ );
+
+ if (!isAgentlessApiEnabled) {
+ logger.error(
+ '[Agentless API] Agentless API is not supported. Deleting agentless agent is not supported in non-cloud or non-serverless environments'
+ );
+ }
+
+ if (!agentlessConfig) {
+ logger.error('[Agentless API] kibana.yml is currently missing Agentless API configuration');
+ }
+
+ logger.debug(`[Agentless API] Deleting agentless agent with TLS config with certificate`);
+
+ logger.debug(
+ `[Agentless API] Deleting agentless deployment with request config ${requestConfigDebugStatus}`
+ );
+
+ const response = await axios(requestConfig).catch((error: AxiosError) => {
+ const errorLogCodeCause = `${error.code} ${this.convertCauseErrorsToString(error)}`;
+
+ if (!axios.isAxiosError(error)) {
+ logger.error(
+ `[Agentless API] Deleting agentless deployment failed with an error ${JSON.stringify(
+ error
+ )} ${requestConfigDebugStatus}`
+ );
+ }
+ if (error.response) {
+ logger.error(
+ `[Agentless API] Deleting Agentless deployment Failed Response Error: ${JSON.stringify(
+ error.response.status
+ )}} ${JSON.stringify(error.response.data)}} ${requestConfigDebugStatus} `
+ );
+ } else if (error.request) {
+ logger.error(
+ `[Agentless API] Deleting agentless deployment failed to receive a response from the Agentless API ${errorLogCodeCause} ${requestConfigDebugStatus}`
+ );
+ } else {
+ logger.error(
+ `[Agentless API] Deleting agentless deployment failed to delete the request ${errorLogCodeCause} ${requestConfigDebugStatus}`
+ );
+ }
+ });
+
return response;
}
+ private createTlsConfig(agentlessConfig: AgentlessConfig | undefined) {
+ return new SslConfig(
+ sslSchema.validate({
+ enabled: true,
+ certificate: agentlessConfig?.api?.tls?.certificate,
+ key: agentlessConfig?.api?.tls?.key,
+ certificateAuthorities: agentlessConfig?.api?.tls?.ca,
+ })
+ );
+ }
+
+ private createRequestConfigDebug(requestConfig: AxiosRequestConfig) {
+ return JSON.stringify({
+ ...requestConfig,
+ data: {
+ ...requestConfig.data,
+ fleet_token: '[REDACTED]',
+ },
+ httpsAgent: {
+ ...requestConfig.httpsAgent,
+ options: {
+ ...requestConfig.httpsAgent.options,
+ cert: requestConfig.httpsAgent.options.cert ? 'REDACTED' : undefined,
+ key: requestConfig.httpsAgent.options.key ? 'REDACTED' : undefined,
+ ca: requestConfig.httpsAgent.options.ca ? 'REDACTED' : undefined,
+ },
+ },
+ });
+ }
+
private convertCauseErrorsToString = (error: AxiosError) => {
if (error.cause instanceof AggregateError) {
return error.cause.errors.map((e: Error) => e.message);
diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts
index 4a7b6c2e2ee70..86d81f3df9b1a 100644
--- a/x-pack/plugins/fleet/server/services/package_policy.ts
+++ b/x-pack/plugins/fleet/server/services/package_policy.ts
@@ -1444,12 +1444,6 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
});
}
- if (agentlessAgentPolicies.length > 0) {
- for (const agentPolicyId of agentlessAgentPolicies) {
- await agentPolicyService.delete(soClient, esClient, agentPolicyId, { force: true });
- }
- }
-
if (!options?.skipUnassignFromAgentPolicies) {
let uniquePolicyIdsR = [
...new Set(
@@ -3021,8 +3015,7 @@ async function validateIsNotHostedPolicy(
throw new AgentPolicyNotFoundError('Agent policy not found');
}
- const isManagedPolicyWithoutServerlessSupport =
- agentPolicy.is_managed && !agentPolicy.supports_agentless && !force;
+ const isManagedPolicyWithoutServerlessSupport = agentPolicy.is_managed && !force;
if (isManagedPolicyWithoutServerlessSupport) {
throw new HostedAgentPolicyRestrictionRelatedError(
diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.ts b/x-pack/plugins/fleet/server/services/utils/agentless.ts
index 5c544b1907b25..c85e9cc991a6c 100644
--- a/x-pack/plugins/fleet/server/services/utils/agentless.ts
+++ b/x-pack/plugins/fleet/server/services/utils/agentless.ts
@@ -28,6 +28,18 @@ const AGENTLESS_SERVERLESS_API_BASE_PATH = '/api/v1/serverless';
type AgentlessApiEndpoints = '/deployments' | `/deployments/${string}`;
+export interface AgentlessConfig {
+ enabled?: boolean;
+ api?: {
+ url?: string;
+ tls?: {
+ certificate?: string;
+ key?: string;
+ ca?: string;
+ };
+ };
+}
+
export const prependAgentlessApiBasePathToEndpoint = (
agentlessConfig: FleetConfigType['agentless'],
endpoint: AgentlessApiEndpoints
@@ -38,3 +50,10 @@ export const prependAgentlessApiBasePathToEndpoint = (
: AGENTLESS_ESS_API_BASE_PATH;
return `${agentlessConfig.api.url}${endpointPrefix}${endpoint}`;
};
+
+export const getDeletionEndpointPath = (
+ agentlessConfig: FleetConfigType['agentless'],
+ endpoint: AgentlessApiEndpoints
+) => {
+ return `${agentlessConfig.api.url}${AGENTLESS_ESS_API_BASE_PATH}${endpoint}`;
+};
diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/columns.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/columns.tsx
index 68ec09897f7c2..99fdc25382bf2 100644
--- a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/columns.tsx
+++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/columns.tsx
@@ -186,7 +186,11 @@ export const getDatasetQualityTableColumns = ({
const { integration, name, rawName } = dataStreamStat;
return (
-
+
diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/dataset_quality_details_link.test.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/dataset_quality_details_link.test.tsx
new file mode 100644
index 0000000000000..ca9dc9764e4b4
--- /dev/null
+++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/dataset_quality_details_link.test.tsx
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { DATA_QUALITY_DETAILS_LOCATOR_ID } from '@kbn/deeplinks-observability';
+import { BrowserUrlService } from '@kbn/share-plugin/public';
+import { shallow } from 'enzyme';
+import React from 'react';
+import { DatasetQualityDetailsLink } from './dataset_quality_details_link';
+
+const createMockLocator = (id: string) => ({
+ navigate: jest.fn(),
+ getRedirectUrl: jest.fn().mockReturnValue(id),
+});
+
+describe('DatasetQualityDetailsLink', () => {
+ const mockDataQualityDetailsLocator = createMockLocator(DATA_QUALITY_DETAILS_LOCATOR_ID);
+
+ const urlServiceMock = {
+ locators: {
+ get: jest.fn((id) => {
+ switch (id) {
+ case DATA_QUALITY_DETAILS_LOCATOR_ID:
+ return mockDataQualityDetailsLocator;
+ default:
+ throw new Error(`Unknown locator id: ${id}`);
+ }
+ }),
+ },
+ } as any as BrowserUrlService;
+
+ const dataStream = {
+ title: 'My data stream',
+ rawName: 'logs-my.data.stream-default',
+ };
+
+ const timeRange = {
+ from: 'now-7d/d',
+ refresh: {
+ pause: true,
+ value: 60000,
+ },
+ to: 'now',
+ };
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders a link to dataset quality details', () => {
+ const wrapper = shallow(
+
+ {dataStream.title}
+
+ );
+
+ expect(mockDataQualityDetailsLocator.getRedirectUrl).toHaveBeenCalledWith({
+ dataStream: dataStream.rawName,
+ timeRange,
+ });
+ expect(wrapper.prop('href')).toBe(DATA_QUALITY_DETAILS_LOCATOR_ID);
+ });
+});
diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/dataset_quality_details_link.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/dataset_quality_details_link.tsx
index ac73f269d9f5a..ec6c34ce1a772 100644
--- a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/dataset_quality_details_link.tsx
+++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/dataset_quality_details_link.tsx
@@ -5,31 +5,34 @@
* 2.0.
*/
-import React from 'react';
-import { BrowserUrlService } from '@kbn/share-plugin/public';
+import { EuiHeaderLink } from '@elastic/eui';
import {
DATA_QUALITY_DETAILS_LOCATOR_ID,
DataQualityDetailsLocatorParams,
} from '@kbn/deeplinks-observability';
import { getRouterLinkProps } from '@kbn/router-utils';
-import { EuiHeaderLink } from '@elastic/eui';
+import { BrowserUrlService } from '@kbn/share-plugin/public';
+import React from 'react';
+import { TimeRangeConfig } from '../../../../common/types';
export const DatasetQualityDetailsLink = React.memo(
({
urlService,
dataStream,
+ timeRange,
children,
}: {
urlService: BrowserUrlService;
dataStream: string;
+ timeRange: TimeRangeConfig;
children: React.ReactNode;
}) => {
const locator = urlService.locators.get(
DATA_QUALITY_DETAILS_LOCATOR_ID
);
- const datasetQualityUrl = locator?.getRedirectUrl({ dataStream });
+ const datasetQualityUrl = locator?.getRedirectUrl({ dataStream, timeRange });
const navigateToDatasetQuality = () => {
- locator?.navigate({ dataStream });
+ locator?.navigate({ dataStream, timeRange });
};
const datasetQualityLinkDetailsProps = getRouterLinkProps({
diff --git a/x-pack/plugins/observability_solution/infra/public/apps/logs_app.tsx b/x-pack/plugins/observability_solution/infra/public/apps/logs_app.tsx
index 8bd243b0723d5..329e059288e3e 100644
--- a/x-pack/plugins/observability_solution/infra/public/apps/logs_app.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/apps/logs_app.tsx
@@ -57,7 +57,7 @@ const LogsApp: React.FC<{
storage: Storage;
theme$: AppMountParameters['theme$'];
}> = ({ core, history, pluginStart, plugins, setHeaderActionMenu, storage, theme$ }) => {
- const uiCapabilities = core.application.capabilities;
+ const { logs, discover, fleet } = core.application.capabilities;
return (
@@ -74,19 +74,21 @@ const LogsApp: React.FC<{
toastsService={core.notifications.toasts}
>
- {
- plugins.share.url.locators
- .get(ALL_DATASETS_LOCATOR_ID)
- ?.navigate({});
+ {Boolean(discover?.show && fleet?.read) && (
+ {
+ plugins.share.url.locators
+ .get(ALL_DATASETS_LOCATOR_ID)
+ ?.navigate({});
- return null;
- }}
- />
+ return null;
+ }}
+ />
+ )}
- {uiCapabilities?.logs?.show && }
+ {logs?.show && }
diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/page_content.tsx
index 056c98513c244..5b5965bb2d5ec 100644
--- a/x-pack/plugins/observability_solution/infra/public/pages/logs/page_content.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/page_content.tsx
@@ -113,6 +113,7 @@ export const LogsPageContent: React.FunctionComponent = () => {
)}
+
(
{
+ const { infrastructure, logs, discover, fleet } = capabilities;
return [
- ...(capabilities.logs.show
+ ...(logs.show
? [
{
label: 'Logs',
sortKey: 200,
entries: [
- {
- label: 'Explorer',
- app: 'observability-logs-explorer',
- path: '/',
- isBetaFeature: true,
- },
+ ...(discover?.show && fleet?.read
+ ? [
+ {
+ label: 'Explorer',
+ app: 'observability-logs-explorer',
+ path: '/',
+ isBetaFeature: true,
+ },
+ ]
+ : []),
...(this.config.featureFlags.logsUIEnabled
? [
{ label: 'Stream', app: 'logs', path: '/stream' },
@@ -161,7 +166,7 @@ export class Plugin implements InfraClientPluginClass {
},
]
: []),
- ...(capabilities.infrastructure.show
+ ...(infrastructure.show
? [
{
label: metricsTitle,
diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/plugin.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/plugin.ts
index 906caf72a450a..798a03da0ebdf 100644
--- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/plugin.ts
+++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/plugin.ts
@@ -7,6 +7,8 @@
import {
AppMountParameters,
+ AppStatus,
+ AppUpdater,
CoreSetup,
CoreStart,
DEFAULT_APP_CATEGORIES,
@@ -14,6 +16,7 @@ import {
PluginInitializerContext,
} from '@kbn/core/public';
import { OBSERVABILITY_LOGS_EXPLORER_APP_ID } from '@kbn/deeplinks-observability';
+import { BehaviorSubject } from 'rxjs';
import {
AllDatasetsLocatorDefinition,
ObservabilityLogsExplorerLocators,
@@ -35,6 +38,7 @@ export class ObservabilityLogsExplorerPlugin
{
private config: ObservabilityLogsExplorerConfig;
private locators?: ObservabilityLogsExplorerLocators;
+ private appStateUpdater = new BehaviorSubject(() => ({}));
constructor(context: PluginInitializerContext) {
this.config = context.config.get();
@@ -56,6 +60,7 @@ export class ObservabilityLogsExplorerPlugin
? ['globalSearch', 'sideNav']
: ['globalSearch'],
keywords: ['logs', 'log', 'explorer', 'logs explorer'],
+ updater$: this.appStateUpdater,
mount: async (appMountParams: ObservabilityLogsExplorerAppMountParameters) => {
const [coreStart, pluginsStart, ownPluginStart] = await core.getStartServices();
const { renderObservabilityLogsExplorer } = await import(
@@ -123,7 +128,16 @@ export class ObservabilityLogsExplorerPlugin
};
}
- public start(_core: CoreStart, _pluginsStart: ObservabilityLogsExplorerStartDeps) {
+ public start(core: CoreStart, _pluginsStart: ObservabilityLogsExplorerStartDeps) {
+ const { discover, fleet, logs } = core.application.capabilities;
+
+ if (!(discover?.show && fleet?.read && logs?.show)) {
+ this.appStateUpdater.next(() => ({
+ status: AppStatus.inaccessible,
+ visibleIn: [],
+ }));
+ }
+
return {};
}
}
diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_rule.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_burn_rate_rule.tsx
similarity index 59%
rename from x-pack/plugins/observability_solution/slo/public/hooks/use_create_rule.ts
rename to x-pack/plugins/observability_solution/slo/public/hooks/use_create_burn_rate_rule.tsx
index a85072d907983..906e844c14806 100644
--- a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_rule.ts
+++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_burn_rate_rule.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import React from 'react';
import { useMutation } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import { BASE_ALERTING_API_PATH, RuleTypeParams } from '@kbn/alerting-plugin/common';
@@ -13,18 +14,23 @@ import type {
CreateRuleRequestBody,
CreateRuleResponse,
} from '@kbn/alerting-plugin/common/routes/rule/apis/create';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
+import { toMountPoint } from '@kbn/react-kibana-mount';
import { useKibana } from '../utils/kibana_react';
export function useCreateRule() {
const {
http,
+ i18n: i18nStart,
notifications: { toasts },
+ theme,
} = useKibana().services;
- const createRule = useMutation<
+ return useMutation<
CreateRuleResponse,
Error,
- { rule: CreateRuleRequestBody }
+ { rule: CreateRuleRequestBody },
+ { loadingToastId?: string }
>(
['createRule'],
({ rule }) => {
@@ -39,6 +45,24 @@ export function useCreateRule() {
}
},
{
+ onMutate: async () => {
+ const loadingToast = toasts.addInfo({
+ title: toMountPoint(
+
+
+ {i18n.translate('xpack.slo.rules.createRule.loadingNotification.descriptionText', {
+ defaultMessage: 'Creating burn rate rule ...',
+ })}
+
+
+
+
+ ,
+ { i18n: i18nStart, theme }
+ ),
+ });
+ return { loadingToastId: loadingToast.id };
+ },
onError: (_err) => {
toasts.addDanger(
i18n.translate('xpack.slo.rules.createRule.errorNotification.descriptionText', {
@@ -54,8 +78,11 @@ export function useCreateRule() {
})
);
},
+ onSettled: (_d, _err, _res, ctx) => {
+ if (ctx?.loadingToastId) {
+ toasts.remove(ctx?.loadingToastId);
+ }
+ },
}
);
-
- return createRule;
}
diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_slo.tsx b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_slo.tsx
index fcb02315a847a..e5b6d2c114a84 100644
--- a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_slo.tsx
+++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_slo.tsx
@@ -55,7 +55,7 @@ export function useCreateSlo() {
-
- {i18n.translate('xpack.slo.sloEdit.groupBy.label', {
- defaultMessage: 'Group by',
- })}{' '}
-
-
- }
- labelAppend={ }
- placeholder={i18n.translate('xpack.slo.sloEdit.groupBy.placeholder', {
- defaultMessage: 'Select an optional field to group by',
- })}
isLoading={!!index && isLoading}
isDisabled={!index}
/>
diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/index_field_selector.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/group_by_field_selector.tsx
similarity index 59%
rename from x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/index_field_selector.tsx
rename to x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/group_by_field_selector.tsx
index 0a277900ac31f..c45cc1d337aad 100644
--- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/index_field_selector.tsx
+++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/group_by_field_selector.tsx
@@ -5,35 +5,27 @@
* 2.0.
*/
-import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
-import React, { useEffect, useState, ReactNode } from 'react';
-import { Controller, useFormContext } from 'react-hook-form';
+import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiIconTip } from '@elastic/eui';
import { FieldSpec } from '@kbn/data-views-plugin/common';
-import { createOptionsFromFields, Option } from '../../helpers/create_options';
+import { i18n } from '@kbn/i18n';
+import { ALL_VALUE } from '@kbn/slo-schema';
+import React, { useEffect, useState } from 'react';
+import { Controller, useFormContext } from 'react-hook-form';
+import { Option, createOptionsFromFields } from '../../helpers/create_options';
import { CreateSLOForm } from '../../types';
+import { OptionalText } from './optional_text';
interface Props {
indexFields: FieldSpec[];
- name: 'groupBy' | 'indicator.params.timestampField';
- label: ReactNode | string;
- placeholder: string;
isDisabled: boolean;
isLoading: boolean;
- isRequired?: boolean;
- defaultValue?: string;
- labelAppend?: ReactNode;
}
-export function IndexFieldSelector({
- indexFields,
- name,
- label,
- labelAppend,
- placeholder,
- isDisabled,
- isLoading,
- isRequired = false,
- defaultValue = '',
-}: Props) {
+
+const placeholder = i18n.translate('xpack.slo.sloEdit.groupBy.placeholder', {
+ defaultMessage: 'Select an optional field to group by',
+});
+
+export function GroupByFieldSelector({ indexFields, isDisabled, isLoading }: Props) {
const { control, getFieldState } = useFormContext();
const [options, setOptions] = useState(createOptionsFromFields(indexFields));
@@ -41,10 +33,10 @@ export function IndexFieldSelector({
setOptions(createOptionsFromFields(indexFields));
}, [indexFields]);
- const getSelectedItems = (value: string | string[], fields: FieldSpec[]) => {
+ const getSelectedItems = (value: string | string[]) => {
const values = [value].flat();
const selectedItems: Array> = [];
- fields.forEach((field) => {
+ indexFields.forEach((field) => {
if (values.includes(field.name)) {
selectedItems.push({ value: field.name, label: field.name });
}
@@ -53,12 +45,27 @@ export function IndexFieldSelector({
};
return (
-
+
+ {i18n.translate('xpack.slo.sloEdit.groupBy.label', {
+ defaultMessage: 'Group by',
+ })}{' '}
+
+
+ }
+ isInvalid={getFieldState('groupBy').invalid}
+ labelAppend={ }
+ >
{
return (
@@ -75,7 +82,7 @@ export function IndexFieldSelector({
return field.onChange(selected.map((selection) => selection.value));
}
- field.onChange(defaultValue);
+ field.onChange([ALL_VALUE]);
}}
options={options}
onSearchChange={(searchValue: string) => {
@@ -83,9 +90,7 @@ export function IndexFieldSelector({
createOptionsFromFields(indexFields, ({ value }) => value.includes(searchValue))
);
}}
- selectedOptions={
- !!indexFields && !!field.value ? getSelectedItems(field.value, indexFields) : []
- }
+ selectedOptions={!!indexFields && !!field.value ? getSelectedItems(field.value) : []}
/>
);
}}
diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/timestamp_field_selector.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/timestamp_field_selector.tsx
new file mode 100644
index 0000000000000..dc3289ca895c1
--- /dev/null
+++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/timestamp_field_selector.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
+import { FieldSpec } from '@kbn/data-views-plugin/common';
+import { i18n } from '@kbn/i18n';
+import React, { useEffect, useState } from 'react';
+import { Controller, useFormContext } from 'react-hook-form';
+import { Option, createOptionsFromFields } from '../../helpers/create_options';
+import { CreateSLOForm } from '../../types';
+
+interface Props {
+ fields: FieldSpec[];
+ isDisabled: boolean;
+ isLoading: boolean;
+}
+
+const placeholder = i18n.translate('xpack.slo.sloEdit.timestampField.placeholder', {
+ defaultMessage: 'Select a timestamp field',
+});
+
+export function TimestampFieldSelector({ fields, isDisabled, isLoading }: Props) {
+ const { control, getFieldState } = useFormContext();
+ const [options, setOptions] = useState(createOptionsFromFields(fields));
+
+ useEffect(() => {
+ setOptions(createOptionsFromFields(fields));
+ }, [fields]);
+
+ return (
+
+ {
+ return (
+
+ {...field}
+ async
+ placeholder={placeholder}
+ aria-label={placeholder}
+ isClearable
+ isDisabled={isLoading || isDisabled}
+ isInvalid={fieldState.invalid}
+ isLoading={isLoading}
+ onChange={(selected: EuiComboBoxOptionOption[]) => {
+ if (selected.length) {
+ return field.onChange(selected[0].value);
+ }
+
+ field.onChange('');
+ }}
+ singleSelection={{ asPlainText: true }}
+ options={options}
+ onSearchChange={(searchValue: string) => {
+ setOptions(
+ createOptionsFromFields(fields, ({ value }) => value.includes(searchValue))
+ );
+ }}
+ selectedOptions={
+ !!fields && !!field.value ? [{ value: field.value, label: field.value }] : []
+ }
+ />
+ );
+ }}
+ />
+
+ );
+}
diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_and_timestamp_field.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_and_timestamp_field.tsx
index b4b5bdd4557d4..7059810a5aae0 100644
--- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_and_timestamp_field.tsx
+++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_and_timestamp_field.tsx
@@ -6,13 +6,12 @@
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import React from 'react';
import { DataView } from '@kbn/data-views-plugin/public';
+import React from 'react';
import { useFormContext } from 'react-hook-form';
-import { IndexSelection } from './index_selection';
-import { IndexFieldSelector } from '../common/index_field_selector';
import { CreateSLOForm } from '../../types';
+import { TimestampFieldSelector } from '../common/timestamp_field_selector';
+import { IndexSelection } from './index_selection';
export function IndexAndTimestampField({
dataView,
@@ -32,18 +31,10 @@ export function IndexAndTimestampField({
-
diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_footer.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_footer.tsx
index f3f18c7f0b332..29150231046d1 100644
--- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_footer.tsx
+++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_footer.tsx
@@ -11,7 +11,7 @@ import type { GetSLOResponse } from '@kbn/slo-schema';
import React, { useCallback, useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { InPortal } from 'react-reverse-portal';
-import { useCreateRule } from '../../../hooks/use_create_rule';
+import { useCreateRule } from '../../../hooks/use_create_burn_rate_rule';
import { useKibana } from '../../../utils/kibana_react';
import { sloEditFormFooterPortal } from '../shared_flyout/slo_add_form_flyout';
import { paths } from '../../../../common/locators/paths';
@@ -32,8 +32,6 @@ export interface Props {
onSave?: () => void;
}
-export const maxWidth = 775;
-
export function SloEditFormFooter({ slo, onSave }: Props) {
const {
application: { navigateToUrl },
@@ -45,7 +43,7 @@ export function SloEditFormFooter({ slo, onSave }: Props) {
const { mutateAsync: createSlo, isLoading: isCreateSloLoading } = useCreateSlo();
const { mutateAsync: updateSlo, isLoading: isUpdateSloLoading } = useUpdateSlo();
- const { mutateAsync: createBurnRateRule, isLoading: isCreateBurnRateRuleLoading } =
+ const { mutate: createBurnRateRule, isLoading: isCreateBurnRateRuleLoading } =
useCreateRule();
const navigate = useCallback(
@@ -70,7 +68,7 @@ export function SloEditFormFooter({ slo, onSave }: Props) {
} else {
const processedValues = transformCreateSLOFormToCreateSLOInput(values);
const resp = await createSlo({ slo: processedValues });
- await createBurnRateRule({
+ createBurnRateRule({
rule: createBurnRateRuleRequestBody({ ...processedValues, id: resp.id }),
});
if (onSave) {
diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx
index dcd497da34770..f5acb4e964f08 100644
--- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx
+++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx
@@ -23,7 +23,7 @@ import { useFetchApmSuggestions } from '../../hooks/use_fetch_apm_suggestions';
import { useFetchIndices } from '../../hooks/use_fetch_indices';
import { useFetchSloDetails } from '../../hooks/use_fetch_slo_details';
import { usePermissions } from '../../hooks/use_permissions';
-import { useCreateRule } from '../../hooks/use_create_rule';
+import { useCreateRule } from '../../hooks/use_create_burn_rate_rule';
import { useUpdateSlo } from '../../hooks/use_update_slo';
import { useKibana } from '../../utils/kibana_react';
import { kibanaStartMock } from '../../utils/kibana_react.mock';
@@ -45,7 +45,7 @@ jest.mock('../../hooks/use_create_slo');
jest.mock('../../hooks/use_update_slo');
jest.mock('../../hooks/use_fetch_apm_suggestions');
jest.mock('../../hooks/use_permissions');
-jest.mock('../../hooks/use_create_rule');
+jest.mock('../../hooks/use_create_burn_rate_rule');
const mockUseKibanaReturnValue = kibanaStartMock.startContract();
diff --git a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/register.ts b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/register.ts
index 4e4c50305ef43..50e09e42512b7 100644
--- a/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/register.ts
+++ b/x-pack/plugins/observability_solution/slo/server/lib/rules/slo_burn_rate/register.ts
@@ -115,7 +115,7 @@ export function sloBurnRateRuleType(
alerts: {
context: SLO_RULE_REGISTRATION_CONTEXT,
mappings: { fieldMap: { ...legacyExperimentalFieldMap, ...sloRuleFieldMap } },
- useEcs: false,
+ useEcs: true,
useLegacyAlerts: true,
shouldWrite: true,
},
diff --git a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts
index fffbd7aa1e924..703c8aab5a684 100644
--- a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts
+++ b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts
@@ -29,6 +29,8 @@ import {
updateSLOParamsSchema,
} from '@kbn/slo-schema';
import { getOverviewParamsSchema } from '@kbn/slo-schema/src/rest_specs/routes/get_overview';
+import { KibanaRequest } from '@kbn/core-http-server';
+import { RegisterRoutesDependencies } from '../register_routes';
import { GetSLOsOverview } from '../../services/get_slos_overview';
import type { IndicatorTypes } from '../../domain/models';
import {
@@ -91,6 +93,11 @@ const assertPlatinumLicense = async (context: SloRequestHandlerContext) => {
}
};
+const getSpaceId = async (deps: RegisterRoutesDependencies, request: KibanaRequest) => {
+ const spaces = await deps.getSpacesStart();
+ return (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default';
+};
+
const createSLORoute = createSloServerRoute({
endpoint: 'POST /api/observability/slos 2023-10-31',
options: {
@@ -101,10 +108,7 @@ const createSLORoute = createSloServerRoute({
handler: async ({ context, params, logger, dependencies, request }) => {
await assertPlatinumLicense(context);
- const spaces = await dependencies.getSpacesStart();
const dataViews = await dependencies.getDataViewsStart();
- const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default';
-
const core = await context.core;
const scopedClusterClient = core.elasticsearch.client;
const esClient = core.elasticsearch.client.asCurrentUser;
@@ -112,7 +116,10 @@ const createSLORoute = createSloServerRoute({
const soClient = core.savedObjects.client;
const repository = new KibanaSavedObjectsSLORepository(soClient, logger);
- const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient);
+ const [spaceId, dataViewsService] = await Promise.all([
+ getSpaceId(dependencies, request),
+ dataViews.dataViewsServiceFactory(soClient, esClient),
+ ]);
const transformManager = new DefaultTransformManager(
transformGenerators,
scopedClusterClient,
@@ -125,7 +132,6 @@ const createSLORoute = createSloServerRoute({
scopedClusterClient,
logger
);
-
const createSLO = new CreateSLO(
esClient,
scopedClusterClient,
@@ -137,9 +143,7 @@ const createSLORoute = createSloServerRoute({
basePath
);
- const response = await createSLO.execute(params.body);
-
- return response;
+ return await createSLO.execute(params.body);
},
});
diff --git a/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts
index 9499294eeb89b..84edf74f18aa5 100644
--- a/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts
+++ b/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts
@@ -65,7 +65,7 @@ describe('CreateSLO', () => {
const response = await createSLO.execute(sloParams);
- expect(mockRepository.save).toHaveBeenCalledWith(
+ expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
...sloParams,
id: 'unique-id',
@@ -80,17 +80,14 @@ describe('CreateSLO', () => {
version: 2,
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
- }),
- { throwOnConflict: true }
+ })
);
expect(mockTransformManager.install).toHaveBeenCalled();
- expect(mockTransformManager.start).toHaveBeenCalled();
expect(
mockScopedClusterClient.asSecondaryAuthUser.ingest.putPipeline.mock.calls[0]
).toMatchSnapshot();
expect(mockSummaryTransformManager.install).toHaveBeenCalled();
- expect(mockSummaryTransformManager.start).toHaveBeenCalled();
expect(mockEsClient.index.mock.calls[0]).toMatchSnapshot();
expect(response).toEqual(expect.objectContaining({ id: 'unique-id' }));
@@ -108,7 +105,7 @@ describe('CreateSLO', () => {
await createSLO.execute(sloParams);
- expect(mockRepository.save).toHaveBeenCalledWith(
+ expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
...sloParams,
id: expect.any(String),
@@ -122,8 +119,7 @@ describe('CreateSLO', () => {
enabled: true,
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
- }),
- { throwOnConflict: true }
+ })
);
});
@@ -141,7 +137,7 @@ describe('CreateSLO', () => {
await createSLO.execute(sloParams);
- expect(mockRepository.save).toHaveBeenCalledWith(
+ expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
...sloParams,
id: expect.any(String),
@@ -155,8 +151,7 @@ describe('CreateSLO', () => {
enabled: true,
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
- }),
- { throwOnConflict: true }
+ })
);
});
});
@@ -173,16 +168,16 @@ describe('CreateSLO', () => {
expect(mockRepository.deleteById).toHaveBeenCalled();
expect(
mockScopedClusterClient.asSecondaryAuthUser.ingest.deletePipeline
- ).toHaveBeenCalledTimes(1);
+ ).toHaveBeenCalledTimes(2);
- expect(mockSummaryTransformManager.stop).not.toHaveBeenCalled();
- expect(mockSummaryTransformManager.uninstall).not.toHaveBeenCalled();
- expect(mockTransformManager.stop).not.toHaveBeenCalled();
- expect(mockTransformManager.uninstall).not.toHaveBeenCalled();
+ expect(mockSummaryTransformManager.stop).toHaveBeenCalledTimes(0);
+ expect(mockSummaryTransformManager.uninstall).toHaveBeenCalledTimes(1);
+ expect(mockTransformManager.stop).toHaveBeenCalledTimes(0);
+ expect(mockTransformManager.uninstall).toHaveBeenCalledTimes(1);
});
- it('rollbacks completed operations when summary transform start fails', async () => {
- mockSummaryTransformManager.start.mockRejectedValue(
+ it('rollbacks completed operations when summary transform install fails', async () => {
+ mockSummaryTransformManager.install.mockRejectedValue(
new Error('Summary transform install error')
);
const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() });
@@ -192,7 +187,7 @@ describe('CreateSLO', () => {
);
expect(mockRepository.deleteById).toHaveBeenCalled();
- expect(mockTransformManager.stop).toHaveBeenCalled();
+ expect(mockTransformManager.stop).not.toHaveBeenCalled();
expect(mockTransformManager.uninstall).toHaveBeenCalled();
expect(
mockScopedClusterClient.asSecondaryAuthUser.ingest.deletePipeline
@@ -211,12 +206,12 @@ describe('CreateSLO', () => {
);
expect(mockRepository.deleteById).toHaveBeenCalled();
- expect(mockTransformManager.stop).toHaveBeenCalled();
+ expect(mockTransformManager.stop).not.toHaveBeenCalled();
expect(mockTransformManager.uninstall).toHaveBeenCalled();
expect(
mockScopedClusterClient.asSecondaryAuthUser.ingest.deletePipeline
).toHaveBeenCalledTimes(2);
- expect(mockSummaryTransformManager.stop).toHaveBeenCalled();
+ expect(mockSummaryTransformManager.stop).not.toHaveBeenCalled();
expect(mockSummaryTransformManager.uninstall).toHaveBeenCalled();
});
});
diff --git a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts
index 0f6885dd44c6c..3845ec2ddbd4f 100644
--- a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts
+++ b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts
@@ -10,6 +10,7 @@ import { ElasticsearchClient, IBasePath, Logger } from '@kbn/core/server';
import { ALL_VALUE, CreateSLOParams, CreateSLOResponse } from '@kbn/slo-schema';
import { asyncForEach } from '@kbn/std';
import { v4 as uuidv4 } from 'uuid';
+import { IngestPutPipelineRequest } from '@elastic/elasticsearch/lib/api/types';
import {
getSLOPipelineId,
getSLOSummaryPipelineId,
@@ -22,7 +23,7 @@ import { getSLOPipelineTemplate } from '../assets/ingest_templates/slo_pipeline_
import { getSLOSummaryPipelineTemplate } from '../assets/ingest_templates/slo_summary_pipeline_template';
import { Duration, DurationUnit, SLODefinition } from '../domain/models';
import { validateSLO } from '../domain/services';
-import { SecurityException } from '../errors';
+import { SecurityException, SLOIdConflict } from '../errors';
import { retryTransientEsErrors } from '../utils/retry';
import { SLORepository } from './slo_repository';
import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary';
@@ -47,62 +48,58 @@ export class CreateSLO {
const rollbackOperations = [];
- await this.repository.save(slo, { throwOnConflict: true });
- rollbackOperations.push(() => this.repository.deleteById(slo.id));
+ const sloAlreadyExists = await this.repository.checkIfSLOExists(slo);
+
+ if (sloAlreadyExists) {
+ throw new SLOIdConflict(`SLO [${slo.id}] already exists`);
+ }
+
+ const createPromise = this.repository.create(slo);
+
+ rollbackOperations.push(() => this.repository.deleteById(slo.id, true));
const rollupTransformId = getSLOTransformId(slo.id, slo.revision);
const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision);
try {
- await retryTransientEsErrors(
- () =>
- this.scopedClusterClient.asSecondaryAuthUser.ingest.putPipeline(
- getSLOPipelineTemplate(slo)
- ),
- { logger: this.logger }
- );
- rollbackOperations.push(() =>
- this.scopedClusterClient.asSecondaryAuthUser.ingest.deletePipeline(
- { id: getSLOPipelineId(slo.id, slo.revision) },
- { ignore: [404] }
- )
- );
+ const sloPipelinePromise = this.createPipeline(getSLOPipelineTemplate(slo));
+ rollbackOperations.push(() => this.deletePipeline(getSLOPipelineId(slo.id, slo.revision)));
- await this.transformManager.install(slo);
+ const rollupTransformPromise = this.transformManager.install(slo);
rollbackOperations.push(() => this.transformManager.uninstall(rollupTransformId));
- await this.transformManager.start(rollupTransformId);
- rollbackOperations.push(() => this.transformManager.stop(rollupTransformId));
-
- await retryTransientEsErrors(
- () =>
- this.scopedClusterClient.asSecondaryAuthUser.ingest.putPipeline(
- getSLOSummaryPipelineTemplate(slo, this.spaceId, this.basePath)
- ),
- { logger: this.logger }
+ const summaryPipelinePromise = this.createPipeline(
+ getSLOSummaryPipelineTemplate(slo, this.spaceId, this.basePath)
);
+
rollbackOperations.push(() =>
- this.scopedClusterClient.asSecondaryAuthUser.ingest.deletePipeline(
- { id: getSLOSummaryPipelineId(slo.id, slo.revision) },
- { ignore: [404] }
- )
+ this.deletePipeline(getSLOSummaryPipelineId(slo.id, slo.revision))
);
- await this.summaryTransformManager.install(slo);
+ const summaryTransformPromise = this.summaryTransformManager.install(slo);
rollbackOperations.push(() => this.summaryTransformManager.uninstall(summaryTransformId));
- await this.summaryTransformManager.start(summaryTransformId);
+ const tempDocPromise = this.createTempSummaryDocument(slo);
+
+ rollbackOperations.push(() => this.deleteTempSummaryDocument(slo));
+
+ await Promise.all([
+ createPromise,
+ sloPipelinePromise,
+ rollupTransformPromise,
+ summaryPipelinePromise,
+ summaryTransformPromise,
+ tempDocPromise,
+ ]);
+
+ rollbackOperations.push(() => this.transformManager.stop(rollupTransformId));
rollbackOperations.push(() => this.summaryTransformManager.stop(summaryTransformId));
- await retryTransientEsErrors(
- () =>
- this.esClient.index({
- index: SLO_SUMMARY_TEMP_INDEX_NAME,
- id: `slo-${slo.id}`,
- document: createTempSummaryDocument(slo, this.spaceId, this.basePath),
- refresh: true,
- }),
- { logger: this.logger }
- );
+ // transforms can only be started after the pipeline is created
+
+ await Promise.all([
+ this.transformManager.start(rollupTransformId),
+ this.summaryTransformManager.start(summaryTransformId),
+ ]);
} catch (err) {
this.logger.error(
`Cannot install the SLO [id: ${slo.id}, revision: ${slo.revision}]. Rolling back.`
@@ -126,6 +123,45 @@ export class CreateSLO {
return this.toResponse(slo);
}
+ async createTempSummaryDocument(slo: SLODefinition) {
+ return await retryTransientEsErrors(
+ () =>
+ this.esClient.index({
+ index: SLO_SUMMARY_TEMP_INDEX_NAME,
+ id: `slo-${slo.id}`,
+ document: createTempSummaryDocument(slo, this.spaceId, this.basePath),
+ refresh: true,
+ }),
+ { logger: this.logger }
+ );
+ }
+
+ async deleteTempSummaryDocument(slo: SLODefinition) {
+ return await retryTransientEsErrors(
+ () =>
+ this.esClient.delete({
+ index: SLO_SUMMARY_TEMP_INDEX_NAME,
+ id: `slo-${slo.id}`,
+ refresh: true,
+ }),
+ { logger: this.logger }
+ );
+ }
+
+ async createPipeline(params: IngestPutPipelineRequest) {
+ return await retryTransientEsErrors(
+ () => this.scopedClusterClient.asSecondaryAuthUser.ingest.putPipeline(params),
+ { logger: this.logger }
+ );
+ }
+
+ async deletePipeline(id: string) {
+ return this.scopedClusterClient.asSecondaryAuthUser.ingest.deletePipeline(
+ { id },
+ { ignore: [404] }
+ );
+ }
+
public async inspect(params: CreateSLOParams): Promise<{
slo: CreateSLOParams;
rollUpPipeline: Record;
diff --git a/x-pack/plugins/observability_solution/slo/server/services/manage_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/manage_slo.test.ts
index bace47b69ff4b..d0fd587f6ed0b 100644
--- a/x-pack/plugins/observability_solution/slo/server/services/manage_slo.test.ts
+++ b/x-pack/plugins/observability_solution/slo/server/services/manage_slo.test.ts
@@ -38,7 +38,7 @@ describe('ManageSLO', () => {
expect(mockTransformManager.start).not.toHaveBeenCalled();
expect(mockSummaryTransformManager.start).not.toHaveBeenCalled();
- expect(mockRepository.save).not.toHaveBeenCalled();
+ expect(mockRepository.create).not.toHaveBeenCalled();
});
it('enables the slo when disabled', async () => {
@@ -49,7 +49,9 @@ describe('ManageSLO', () => {
expect(mockTransformManager.start).toMatchSnapshot();
expect(mockSummaryTransformManager.start).toMatchSnapshot();
- expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({ enabled: true }));
+ expect(mockRepository.update).toHaveBeenCalledWith(
+ expect.objectContaining({ enabled: true })
+ );
});
});
@@ -62,7 +64,7 @@ describe('ManageSLO', () => {
expect(mockTransformManager.stop).not.toHaveBeenCalled();
expect(mockSummaryTransformManager.stop).not.toHaveBeenCalled();
- expect(mockRepository.save).not.toHaveBeenCalled();
+ expect(mockRepository.update).not.toHaveBeenCalled();
});
it('disables the slo when enabled', async () => {
@@ -73,7 +75,9 @@ describe('ManageSLO', () => {
expect(mockTransformManager.stop).toMatchSnapshot();
expect(mockSummaryTransformManager.stop).toMatchSnapshot();
- expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({ enabled: false }));
+ expect(mockRepository.update).toHaveBeenCalledWith(
+ expect.objectContaining({ enabled: false })
+ );
});
});
});
diff --git a/x-pack/plugins/observability_solution/slo/server/services/manage_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/manage_slo.ts
index 84e8e3798598b..65f59832b57a6 100644
--- a/x-pack/plugins/observability_solution/slo/server/services/manage_slo.ts
+++ b/x-pack/plugins/observability_solution/slo/server/services/manage_slo.ts
@@ -26,7 +26,7 @@ export class ManageSLO {
await this.transformManager.start(getSLOTransformId(slo.id, slo.revision));
slo.enabled = true;
slo.updatedAt = new Date();
- await this.repository.save(slo);
+ await this.repository.update(slo);
}
async disable(sloId: string) {
@@ -39,6 +39,6 @@ export class ManageSLO {
await this.transformManager.stop(getSLOTransformId(slo.id, slo.revision));
slo.enabled = false;
slo.updatedAt = new Date();
- await this.repository.save(slo);
+ await this.repository.update(slo);
}
}
diff --git a/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts b/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts
index 4fc9d268f62dd..dc458fcdb813e 100644
--- a/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts
+++ b/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts
@@ -42,11 +42,13 @@ const createSummaryTransformManagerMock = (): jest.Mocked => {
const createSLORepositoryMock = (): jest.Mocked => {
return {
- save: jest.fn(),
+ create: jest.fn(),
+ update: jest.fn(),
findById: jest.fn(),
findAllByIds: jest.fn(),
deleteById: jest.fn(),
search: jest.fn(),
+ checkIfSLOExists: jest.fn(),
};
};
diff --git a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts
index efbf3eedb52e1..4e66d992b46cd 100644
--- a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts
+++ b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts
@@ -63,7 +63,7 @@ describe('ResetSLO', () => {
it('resets all associated resources', async () => {
const slo = createSLO({ id: 'irrelevant', version: 1 });
mockRepository.findById.mockResolvedValueOnce(slo);
- mockRepository.save.mockImplementation((v) => Promise.resolve(v));
+ mockRepository.update.mockImplementation((v) => Promise.resolve(v));
await resetSLO.execute(slo.id);
@@ -87,7 +87,7 @@ describe('ResetSLO', () => {
expect(mockEsClient.index).toMatchSnapshot();
- expect(mockRepository.save).toHaveBeenCalledWith({
+ expect(mockRepository.update).toHaveBeenCalledWith({
...slo,
version: SLO_MODEL_VERSION,
updatedAt: expect.anything(),
diff --git a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts
index f69651ff2ad8a..634f02c8f6f90 100644
--- a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts
+++ b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts
@@ -104,7 +104,7 @@ export class ResetSLO {
throw err;
}
- const updatedSlo = await this.repository.save({
+ const updatedSlo = await this.repository.update({
...slo,
version: SLO_MODEL_VERSION,
updatedAt: new Date(),
diff --git a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts
index 1b6eec0ef4f97..243b2b5e9958b 100644
--- a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts
+++ b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts
@@ -11,7 +11,7 @@ import { MockedLogger } from '@kbn/logging-mocks';
import { sloDefinitionSchema } from '@kbn/slo-schema';
import { SLO_MODEL_VERSION } from '../../common/constants';
import { SLODefinition, StoredSLODefinition } from '../domain/models';
-import { SLOIdConflict, SLONotFound } from '../errors';
+import { SLONotFound } from '../errors';
import { SO_SLO_TYPE } from '../saved_objects';
import { aStoredSLO, createAPMTransactionDurationIndicator, createSLO } from './fixtures/slo';
import { KibanaSavedObjectsSLORepository } from './slo_repository';
@@ -82,43 +82,45 @@ describe('KibanaSavedObjectsSLORepository', () => {
});
describe('saving an SLO', () => {
- it('saves the new SLO', async () => {
+ it('checking existing id for slo', async () => {
const slo = createSLO({ id: 'my-id' });
soClientMock.find.mockResolvedValueOnce(soFindResponse([]));
soClientMock.create.mockResolvedValueOnce(aStoredSLO(slo));
const repository = new KibanaSavedObjectsSLORepository(soClientMock, loggerMock);
- const savedSLO = await repository.save(slo);
+ await repository.checkIfSLOExists(slo);
- expect(savedSLO).toEqual(slo);
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
- page: 1,
- perPage: 1,
+ perPage: 0,
filter: `slo.attributes.id:(${slo.id})`,
});
+ });
+
+ it('saves the new SLO', async () => {
+ const slo = createSLO({ id: 'my-id' });
+ soClientMock.find.mockResolvedValueOnce(soFindResponse([]));
+ soClientMock.create.mockResolvedValueOnce(aStoredSLO(slo));
+ const repository = new KibanaSavedObjectsSLORepository(soClientMock, loggerMock);
+
+ const savedSLO = await repository.create(slo);
+
+ expect(savedSLO).toEqual(slo);
expect(soClientMock.create).toHaveBeenCalledWith(
SO_SLO_TYPE,
- sloDefinitionSchema.encode(slo),
- {
- id: undefined,
- overwrite: true,
- }
+ sloDefinitionSchema.encode(slo)
);
});
- it('throws when the SLO id already exists and "throwOnConflict" is true', async () => {
+ it('checks when the SLO id already exists', async () => {
const slo = createSLO({ id: 'my-id' });
soClientMock.find.mockResolvedValueOnce(soFindResponse([slo]));
const repository = new KibanaSavedObjectsSLORepository(soClientMock, loggerMock);
- await expect(repository.save(slo, { throwOnConflict: true })).rejects.toThrowError(
- new SLOIdConflict(`SLO [my-id] already exists`)
- );
+ await expect(await repository.checkIfSLOExists(slo)).toEqual(true);
expect(soClientMock.find).toHaveBeenCalledWith({
type: SO_SLO_TYPE,
- page: 1,
- perPage: 1,
+ perPage: 0,
filter: `slo.attributes.id:(${slo.id})`,
});
});
@@ -129,15 +131,10 @@ describe('KibanaSavedObjectsSLORepository', () => {
soClientMock.create.mockResolvedValueOnce(aStoredSLO(slo));
const repository = new KibanaSavedObjectsSLORepository(soClientMock, loggerMock);
- const savedSLO = await repository.save(slo);
+ const savedSLO = await repository.update(slo);
expect(savedSLO).toEqual(slo);
- expect(soClientMock.find).toHaveBeenCalledWith({
- type: SO_SLO_TYPE,
- page: 1,
- perPage: 1,
- filter: `slo.attributes.id:(${slo.id})`,
- });
+
expect(soClientMock.create).toHaveBeenCalledWith(
SO_SLO_TYPE,
sloDefinitionSchema.encode(slo),
diff --git a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts
index 9f300a148ac2e..35266ea993bfb 100644
--- a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts
+++ b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts
@@ -11,14 +11,16 @@ import { ALL_VALUE, Paginated, Pagination, sloDefinitionSchema } from '@kbn/slo-
import { isLeft } from 'fp-ts/lib/Either';
import { SLO_MODEL_VERSION } from '../../common/constants';
import { SLODefinition, StoredSLODefinition } from '../domain/models';
-import { SLOIdConflict, SLONotFound } from '../errors';
+import { SLONotFound } from '../errors';
import { SO_SLO_TYPE } from '../saved_objects';
export interface SLORepository {
- save(slo: SLODefinition, options?: { throwOnConflict: boolean }): Promise;
+ checkIfSLOExists(slo: SLODefinition): Promise;
+ create(slo: SLODefinition): Promise;
+ update(slo: SLODefinition): Promise;
findAllByIds(ids: string[]): Promise;
findById(id: string): Promise;
- deleteById(id: string): Promise;
+ deleteById(id: string, ignoreNotFound?: boolean): Promise;
search(
search: string,
pagination: Pagination,
@@ -29,19 +31,30 @@ export interface SLORepository {
export class KibanaSavedObjectsSLORepository implements SLORepository {
constructor(private soClient: SavedObjectsClientContract, private logger: Logger) {}
- async save(slo: SLODefinition, options = { throwOnConflict: false }): Promise {
- let existingSavedObjectId;
+ async checkIfSLOExists(slo: SLODefinition) {
+ const findResponse = await this.soClient.find({
+ type: SO_SLO_TYPE,
+ perPage: 0,
+ filter: `slo.attributes.id:(${slo.id})`,
+ });
+
+ return findResponse.total > 0;
+ }
+
+ async create(slo: SLODefinition): Promise {
+ await this.soClient.create(SO_SLO_TYPE, toStoredSLO(slo));
+ return slo;
+ }
+
+ async update(slo: SLODefinition): Promise {
+ let existingSavedObjectId: string | undefined;
+
const findResponse = await this.soClient.find({
type: SO_SLO_TYPE,
- page: 1,
perPage: 1,
filter: `slo.attributes.id:(${slo.id})`,
});
if (findResponse.total === 1) {
- if (options.throwOnConflict) {
- throw new SLOIdConflict(`SLO [${slo.id}] already exists`);
- }
-
existingSavedObjectId = findResponse.saved_objects[0].id;
}
@@ -73,7 +86,7 @@ export class KibanaSavedObjectsSLORepository implements SLORepository {
return slo;
}
- async deleteById(id: string): Promise {
+ async deleteById(id: string, ignoreNotFound = false): Promise {
const response = await this.soClient.find({
type: SO_SLO_TYPE,
page: 1,
@@ -82,6 +95,9 @@ export class KibanaSavedObjectsSLORepository implements SLORepository {
});
if (response.total === 0) {
+ if (ignoreNotFound) {
+ return;
+ }
throw new SLONotFound(`SLO [${id}] not found`);
}
diff --git a/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts
index 37855bd4d8fa4..dccfe5f97d633 100644
--- a/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts
+++ b/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts
@@ -204,7 +204,7 @@ describe('UpdateSLO', () => {
await updateSLO.execute(slo.id, { settings: newSettings });
expectDeletionOfOriginalSLOResources(slo);
- expect(mockRepository.save).toHaveBeenCalledWith(
+ expect(mockRepository.update).toHaveBeenCalledWith(
expect.objectContaining({
...slo,
settings: newSettings,
@@ -316,7 +316,7 @@ describe('UpdateSLO', () => {
updateSLO.execute(originalSlo.id, { indicator: newIndicator })
).rejects.toThrowError('Transform install error');
- expect(mockRepository.save).toHaveBeenCalledWith(originalSlo);
+ expect(mockRepository.update).toHaveBeenCalledWith(originalSlo);
expect(
mockScopedClusterClient.asSecondaryAuthUser.ingest.deletePipeline
).toHaveBeenCalledTimes(1); // for the sli only
@@ -343,7 +343,7 @@ describe('UpdateSLO', () => {
updateSLO.execute(originalSlo.id, { indicator: newIndicator })
).rejects.toThrowError('summary transform start error');
- expect(mockRepository.save).toHaveBeenCalledWith(originalSlo);
+ expect(mockRepository.update).toHaveBeenCalledWith(originalSlo);
expect(mockSummaryTransformManager.uninstall).toHaveBeenCalled();
expect(mockScopedClusterClient.asSecondaryAuthUser.ingest.deletePipeline).toHaveBeenCalled();
expect(mockTransformManager.stop).toHaveBeenCalled();
diff --git a/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts
index 0f1967800d1df..9418bfb1ea91a 100644
--- a/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts
+++ b/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts
@@ -70,8 +70,8 @@ export class UpdateSLO {
const rollbackOperations = [];
- await this.repository.save(updatedSlo);
- rollbackOperations.push(() => this.repository.save(originalSlo));
+ await this.repository.update(updatedSlo);
+ rollbackOperations.push(() => this.repository.update(originalSlo));
if (!requireRevisionBump) {
// At this point, we still need to update the sli and summary pipeline to include the changes (id and revision in the rollup index) and (name, desc, tags, ...) in the summary index
diff --git a/x-pack/plugins/observability_solution/synthetics/common/constants/client_defaults.ts b/x-pack/plugins/observability_solution/synthetics/common/constants/client_defaults.ts
index 07d17554e83fc..f1098c89b7caa 100644
--- a/x-pack/plugins/observability_solution/synthetics/common/constants/client_defaults.ts
+++ b/x-pack/plugins/observability_solution/synthetics/common/constants/client_defaults.ts
@@ -54,44 +54,32 @@ export const FINAL_SUMMARY_FILTER = {
},
},
{
- bool: {
- should: [
- {
- bool: {
- should: [
- {
- match: {
- 'summary.final_attempt': true,
- },
- },
- ],
- minimum_should_match: 1,
- },
- },
- {
- bool: {
- must_not: {
- bool: {
- should: [
- {
- exists: {
- field: 'summary.final_attempt',
- },
- },
- ],
- minimum_should_match: 1,
- },
- },
- },
- },
- ],
- minimum_should_match: 1,
+ term: {
+ 'summary.final_attempt': true,
},
},
],
},
};
+export const getRangeFilter = ({ from, to }: { from: string; to: string }) => ({
+ range: {
+ '@timestamp': {
+ gte: from,
+ lte: to,
+ },
+ },
+});
+
+export const getTimespanFilter = ({ from, to }: { from: string; to: string }) => ({
+ range: {
+ 'monitor.timespan': {
+ gte: from,
+ lte: to,
+ },
+ },
+});
+
export const SUMMARY_FILTER = { exists: { field: 'summary' } };
export const getLocationFilter = ({
diff --git a/x-pack/plugins/observability_solution/synthetics/common/field_names.ts b/x-pack/plugins/observability_solution/synthetics/common/field_names.ts
index 0407fad341d8a..7b590b3a828a7 100644
--- a/x-pack/plugins/observability_solution/synthetics/common/field_names.ts
+++ b/x-pack/plugins/observability_solution/synthetics/common/field_names.ts
@@ -11,6 +11,7 @@ export const MONITOR_NAME = 'monitor.name';
export const MONITOR_TYPE = 'monitor.type';
export const URL_FULL = 'url.full';
export const URL_PORT = 'url.port';
+export const OBSERVER_NAME = 'observer.name';
export const OBSERVER_GEO_NAME = 'observer.geo.name';
export const ERROR_MESSAGE = 'error.message';
export const STATE_ID = 'monitor.state.id';
diff --git a/x-pack/plugins/observability_solution/synthetics/common/rules/alert_actions.test.ts b/x-pack/plugins/observability_solution/synthetics/common/rules/alert_actions.test.ts
index 345342d1f4c62..dd7ad03b9ac32 100644
--- a/x-pack/plugins/observability_solution/synthetics/common/rules/alert_actions.test.ts
+++ b/x-pack/plugins/observability_solution/synthetics/common/rules/alert_actions.test.ts
@@ -53,7 +53,7 @@ describe('Alert Actions factory', () => {
dedupKey: expect.any(String),
eventAction: 'resolve',
summary:
- 'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
+ 'The alert for monitor "{{context.monitorName}}" from {{context.locationNames}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationNames}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
},
},
{
@@ -193,7 +193,7 @@ describe('Alert Actions factory', () => {
dedupKey: expect.any(String),
eventAction: 'resolve',
summary:
- 'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
+ 'The alert for monitor "{{context.monitorName}}" from {{context.locationNames}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationNames}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
},
},
{
@@ -230,8 +230,7 @@ describe('Alert Actions factory', () => {
dedupKey: 'always-downxpack.uptime.alerts.actionGroups.monitorStatus',
eventAction: 'trigger',
severity: 'error',
- summary:
- 'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}.\n\nDetails:\n\nMonitor name: {{context.monitorName}}\n{{context.monitorUrlLabel}}: {{{context.monitorUrl}}}\nMonitor type: {{context.monitorType}}\nFrom: {{context.locationName}}\nLatest error received: {{{context.lastErrorMessage}}}\n{{{context.linkMessage}}}',
+ summary: SyntheticsMonitorStatusTranslations.defaultRecoveryMessage,
},
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
},
@@ -263,11 +262,9 @@ describe('Alert Actions factory', () => {
path: '',
text: '',
},
- message:
- 'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
+ message: SyntheticsMonitorStatusTranslations.defaultRecoveryMessage,
messageHTML: null,
- subject:
- '"{{context.monitorName}}" ({{context.locationName}}) {{context.recoveryStatus}} - Elastic Synthetics',
+ subject: SyntheticsMonitorStatusTranslations.defaultRecoverySubjectMessage,
to: ['test@email.com'],
},
},
@@ -286,11 +283,9 @@ describe('Alert Actions factory', () => {
path: '',
text: '',
},
- message:
- '"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
+ message: SyntheticsMonitorStatusTranslations.defaultActionMessage,
messageHTML: null,
- subject:
- '"{{context.monitorName}}" ({{context.locationName}}) is down - Elastic Synthetics',
+ subject: SyntheticsMonitorStatusTranslations.defaultSubjectMessage,
to: ['test@email.com'],
},
},
diff --git a/x-pack/plugins/observability_solution/synthetics/common/rules/status_rule.test.ts b/x-pack/plugins/observability_solution/synthetics/common/rules/status_rule.test.ts
new file mode 100644
index 0000000000000..67292f67283ad
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/common/rules/status_rule.test.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { getConditionType } from './status_rule';
+
+describe('Status Rule', () => {
+ it('should return the correct condition type for empty', () => {
+ const { useLatestChecks } = getConditionType({} as any);
+ expect(useLatestChecks).toBe(true);
+ });
+
+ it('should return the correct condition type check based', () => {
+ const { useLatestChecks, useTimeWindow } = getConditionType({
+ window: {
+ numberOfChecks: 5,
+ },
+ });
+ expect(useLatestChecks).toBe(true);
+ expect(useTimeWindow).toBe(false);
+ });
+
+ it('should return the correct condition type time based', () => {
+ const { useTimeWindow, useLatestChecks } = getConditionType({
+ window: {
+ time: {
+ unit: 'm',
+ size: 5,
+ },
+ },
+ });
+ expect(useTimeWindow).toBe(true);
+ expect(useLatestChecks).toBe(false);
+ });
+});
diff --git a/x-pack/plugins/observability_solution/synthetics/common/rules/status_rule.ts b/x-pack/plugins/observability_solution/synthetics/common/rules/status_rule.ts
index 375e0c0dd08c1..584888353cbc4 100644
--- a/x-pack/plugins/observability_solution/synthetics/common/rules/status_rule.ts
+++ b/x-pack/plugins/observability_solution/synthetics/common/rules/status_rule.ts
@@ -6,7 +6,102 @@
*/
import { schema, TypeOf } from '@kbn/config-schema';
+import { isEmpty } from 'lodash';
-export const StatusRulePramsSchema = schema.object({});
+export const TimeWindowSchema = schema.object({
+ unit: schema.oneOf(
+ [schema.literal('s'), schema.literal('m'), schema.literal('h'), schema.literal('d')],
+ {
+ defaultValue: 'm',
+ }
+ ),
+ size: schema.number({
+ defaultValue: 5,
+ }),
+});
+export const NumberOfChecksSchema = schema.object({
+ numberOfChecks: schema.number({
+ defaultValue: 5,
+ min: 1,
+ max: 100,
+ }),
+});
+
+export const StatusRuleConditionSchema = schema.object({
+ groupBy: schema.maybe(
+ schema.string({
+ defaultValue: 'locationId',
+ })
+ ),
+ downThreshold: schema.maybe(
+ schema.number({
+ defaultValue: 3,
+ })
+ ),
+ locationsThreshold: schema.maybe(
+ schema.number({
+ defaultValue: 1,
+ })
+ ),
+ window: schema.oneOf([
+ schema.object({
+ time: TimeWindowSchema,
+ }),
+ NumberOfChecksSchema,
+ ]),
+ includeRetests: schema.maybe(schema.boolean()),
+});
+
+export const StatusRulePramsSchema = schema.object({
+ condition: schema.maybe(StatusRuleConditionSchema),
+ monitorIds: schema.maybe(schema.arrayOf(schema.string())),
+ locations: schema.maybe(schema.arrayOf(schema.string())),
+ tags: schema.maybe(schema.arrayOf(schema.string())),
+ monitorTypes: schema.maybe(schema.arrayOf(schema.string())),
+ projects: schema.maybe(schema.arrayOf(schema.string())),
+ kqlQuery: schema.maybe(schema.string()),
+});
+
+export type TimeWindow = TypeOf;
export type StatusRuleParams = TypeOf;
+export type StatusRuleCondition = TypeOf;
+
+export const getConditionType = (condition?: StatusRuleCondition) => {
+ let numberOfChecks = 1;
+ let timeWindow: TimeWindow = { unit: 'm', size: 1 };
+ if (isEmpty(condition) || !condition?.window) {
+ return {
+ isLocationBased: false,
+ useTimeWindow: false,
+ timeWindow,
+ useLatestChecks: true,
+ numberOfChecks,
+ downThreshold: 1,
+ locationsThreshold: 1,
+ isDefaultRule: true,
+ };
+ }
+ const useTimeWindow = condition.window && 'time' in condition.window;
+ const useLatestChecks = condition.window && 'numberOfChecks' in condition.window;
+
+ if (useLatestChecks) {
+ numberOfChecks =
+ condition && 'numberOfChecks' in condition.window ? condition.window.numberOfChecks : 1;
+ }
+
+ if (useTimeWindow) {
+ timeWindow = condition.window.time;
+ numberOfChecks = condition?.downThreshold ?? 1;
+ }
+
+ return {
+ useTimeWindow,
+ timeWindow,
+ useLatestChecks,
+ numberOfChecks,
+ locationsThreshold: condition?.locationsThreshold ?? 1,
+ downThreshold: condition?.downThreshold ?? 1,
+ isDefaultRule: isEmpty(condition),
+ };
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/common/rules/synthetics/translations.ts b/x-pack/plugins/observability_solution/synthetics/common/rules/synthetics/translations.ts
index 84a02ac9b7f92..0161b55273a01 100644
--- a/x-pack/plugins/observability_solution/synthetics/common/rules/synthetics/translations.ts
+++ b/x-pack/plugins/observability_solution/synthetics/common/rules/synthetics/translations.ts
@@ -12,7 +12,7 @@ export const SyntheticsMonitorStatusTranslations = {
'xpack.synthetics.alerts.syntheticsMonitorStatus.defaultActionMessage',
{
// the extra spaces before `\n` are needed to properly convert this from markdown to an HTML email
- defaultMessage: `"{monitorName}" is {status} from {locationName}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- Monitor type: {monitorType} \n- Checked at: {checkedAt} \n- From: {locationName} \n- Error received: {lastErrorMessage} \n{linkMessage}`,
+ defaultMessage: `Monitor "{monitorName}" is {status} from {locationNames}.{pendingLastRunAt} - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- Monitor type: {monitorType} \n- Checked at: {checkedAt} \n- From: {locationNames} \n- Reason: {reason} \n- Error received: {lastErrorMessage} \n{linkMessage}`,
values: {
monitorName: '{{context.monitorName}}',
monitorType: '{{context.monitorType}}',
@@ -20,29 +20,32 @@ export const SyntheticsMonitorStatusTranslations = {
monitorUrlLabel: '{{context.monitorUrlLabel}}',
status: '{{{context.status}}}',
lastErrorMessage: '{{{context.lastErrorMessage}}}',
- locationName: '{{context.locationName}}',
+ locationNames: '{{context.locationNames}}',
checkedAt: '{{context.checkedAt}}',
linkMessage: '{{{context.linkMessage}}}',
+ pendingLastRunAt: '{{{context.pendingLastRunAt}}}',
+ reason: '{{{context.reason}}}',
},
}
),
defaultSubjectMessage: i18n.translate(
'xpack.synthetics.alerts.syntheticsMonitorStatus.defaultSubjectMessage',
{
- defaultMessage: '"{monitorName}" ({locationName}) is down - Elastic Synthetics',
+ defaultMessage: 'Monitor "{monitorName}" ({locationNames}) is down - Elastic Synthetics',
values: {
monitorName: '{{context.monitorName}}',
- locationName: '{{context.locationName}}',
+ locationNames: '{{context.locationNames}}',
},
}
),
defaultRecoverySubjectMessage: i18n.translate(
'xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoverySubjectMessage',
{
- defaultMessage: '"{monitorName}" ({locationName}) {recoveryStatus} - Elastic Synthetics',
+ defaultMessage:
+ 'Monitor "{monitorName}" ({locationNames}) {recoveryStatus} - Elastic Synthetics',
values: {
recoveryStatus: '{{context.recoveryStatus}}',
- locationName: '{{context.locationName}}',
+ locationNames: '{{context.locationNames}}',
monitorName: '{{context.monitorName}}',
},
}
@@ -52,13 +55,13 @@ export const SyntheticsMonitorStatusTranslations = {
{
// the extra spaces before `\n` are needed to properly convert this from markdown to an HTML email
defaultMessage:
- 'The alert for "{monitorName}" from {locationName} is no longer active: {recoveryReason}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationName} \n- Last error received: {lastErrorMessage} \n{linkMessage}',
+ 'The alert for monitor "{monitorName}" from {locationNames} is no longer active: {recoveryReason}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationNames} \n- Last error received: {lastErrorMessage} \n{linkMessage}',
values: {
monitorName: '{{context.monitorName}}',
monitorUrlLabel: '{{context.monitorUrlLabel}}',
monitorUrl: '{{{context.monitorUrl}}}',
monitorType: '{{context.monitorType}}',
- locationName: '{{context.locationName}}',
+ locationNames: '{{context.locationNames}}',
recoveryReason: '{{context.recoveryReason}}',
lastErrorMessage: '{{{context.lastErrorMessage}}}',
linkMessage: '{{{context.linkMessage}}}',
@@ -75,7 +78,7 @@ export const SyntheticsMonitorStatusTranslations = {
export const TlsTranslations = {
defaultActionMessage: i18n.translate('xpack.synthetics.rules.tls.defaultActionMessage', {
- defaultMessage: `TLS certificate {commonName} {status} - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- Common name: {commonName}\n- Issuer: {issuer}\n- Monitor: {monitorName} \n- Monitor URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationName}`,
+ defaultMessage: `TLS certificate {commonName} {status} - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- Common name: {commonName}\n- Issuer: {issuer}\n- Monitor: {monitorName} \n- Monitor URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationNames}`,
values: {
commonName: '{{context.commonName}}',
issuer: '{{context.issuer}}',
@@ -84,11 +87,11 @@ export const TlsTranslations = {
monitorName: '{{context.monitorName}}',
monitorUrl: '{{{context.monitorUrl}}}',
monitorType: '{{context.monitorType}}',
- locationName: '{{context.locationName}}',
+ locationNames: '{{context.locationNames}}',
},
}),
defaultRecoveryMessage: i18n.translate('xpack.synthetics.rules.tls.defaultRecoveryMessage', {
- defaultMessage: `TLS alert for monitor "{monitorName}" has recovered - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- New status : {newStatus}\n- Previous status: {previousStatus}\n- Monitor: {monitorName} \n- URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationName}`,
+ defaultMessage: `TLS alert for monitor "{monitorName}" has recovered - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- New status : {newStatus}\n- Previous status: {previousStatus}\n- Monitor: {monitorName} \n- URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationNames}`,
values: {
summary: '{{context.summary}}',
previousStatus: '{{context.previousStatus}}',
@@ -96,7 +99,7 @@ export const TlsTranslations = {
monitorName: '{{context.monitorName}}',
monitorUrl: '{{{context.monitorUrl}}}',
monitorType: '{{context.monitorType}}',
- locationName: '{{context.locationName}}',
+ locationNames: '{{context.locationNames}}',
},
}),
name: i18n.translate('xpack.synthetics.rules.tls.clientName', {
diff --git a/x-pack/plugins/observability_solution/synthetics/common/rules/synthetics_rule_field_map.ts b/x-pack/plugins/observability_solution/synthetics/common/rules/synthetics_rule_field_map.ts
index 97ed491b320c9..79d83359132dc 100644
--- a/x-pack/plugins/observability_solution/synthetics/common/rules/synthetics_rule_field_map.ts
+++ b/x-pack/plugins/observability_solution/synthetics/common/rules/synthetics_rule_field_map.ts
@@ -17,8 +17,14 @@ export const syntheticsRuleFieldMap: FieldMap = {
type: 'keyword',
required: false,
},
+ 'observer.name': {
+ type: 'keyword',
+ array: true,
+ required: false,
+ },
'observer.geo.name': {
type: 'keyword',
+ array: true,
required: false,
},
// monitor status alert fields
@@ -43,6 +49,10 @@ export const syntheticsRuleFieldMap: FieldMap = {
array: true,
required: false,
},
+ 'monitor.state.id': {
+ type: 'keyword',
+ required: false,
+ },
configId: {
type: 'keyword',
required: false,
@@ -53,10 +63,12 @@ export const syntheticsRuleFieldMap: FieldMap = {
},
'location.id': {
type: 'keyword',
+ array: true,
required: false,
},
'location.name': {
type: 'keyword',
+ array: true,
required: false,
},
// tls alert fields
diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/alert_rules/common.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/alert_rules/common.ts
index 790ce35264752..6a07615ac1644 100644
--- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/alert_rules/common.ts
+++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/alert_rules/common.ts
@@ -25,14 +25,7 @@ export const SyntheticsCommonStateCodec = t.intersection([
export type SyntheticsCommonState = t.TypeOf;
-export const SyntheticsMonitorStatusAlertStateCodec = t.type({
- configId: t.string,
- locationId: t.string,
- locationName: t.string,
- errorStartedAt: t.string,
- lastErrorMessage: t.string,
- stateId: t.string,
-});
+export const SyntheticsMonitorStatusAlertStateCodec = t.type({});
export type SyntheticsMonitorStatusAlertState = t.TypeOf<
typeof SyntheticsMonitorStatusAlertStateCodec
@@ -45,6 +38,10 @@ export const AlertStatusMetaDataCodec = t.interface({
locationId: t.string,
timestamp: t.string,
ping: OverviewPingCodec,
+ checks: t.type({
+ downWithinXChecks: t.number,
+ down: t.number,
+ }),
});
export const StaleAlertStatusMetaDataCodec = t.intersection([
@@ -69,9 +66,6 @@ export const AlertPendingStatusMetaDataCodec = t.intersection([
]);
export const AlertStatusCodec = t.interface({
- up: t.number,
- down: t.number,
- pending: t.number,
upConfigs: t.record(t.string, AlertStatusMetaDataCodec),
downConfigs: t.record(t.string, AlertStatusMetaDataCodec),
pendingConfigs: t.record(t.string, AlertPendingStatusMetaDataCodec),
@@ -79,7 +73,7 @@ export const AlertStatusCodec = t.interface({
staleDownConfigs: t.record(t.string, StaleAlertStatusMetaDataCodec),
});
-export type AlertPendingStatusMetaData = t.TypeOf;
export type StaleDownConfig = t.TypeOf;
export type AlertStatusMetaData = t.TypeOf;
export type AlertOverviewStatus = t.TypeOf;
+export type AlertStatusConfigs = Record;
diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts
index cf06fb899c948..073a00e5665df 100644
--- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts
+++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts
@@ -96,6 +96,10 @@ export const MonitorType = t.intersection([
status: t.string,
type: t.string,
check_group: t.string,
+ timespan: t.type({
+ gte: t.string,
+ lt: t.string,
+ }),
}),
t.partial({
duration: t.type({
@@ -103,10 +107,7 @@ export const MonitorType = t.intersection([
}),
ip: t.string,
name: t.string,
- timespan: t.type({
- gte: t.string,
- lt: t.string,
- }),
+
fleet_managed: t.boolean,
project: t.type({
id: t.string,
diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts
new file mode 100644
index 0000000000000..161a58d650e6c
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { journey, step, before, after, expect } from '@elastic/synthetics';
+import { RetryService } from '@kbn/ftr-common-functional-services';
+import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app';
+import { SyntheticsServices } from '../services/synthetics_services';
+
+journey(`CustomStatusAlert`, async ({ page, params }) => {
+ const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl, params });
+
+ const services = new SyntheticsServices(params);
+ const getService = params.getService;
+ const retry: RetryService = getService('retry');
+
+ const firstCheckTime = new Date(Date.now()).toISOString();
+
+ let configId: string;
+
+ before(async () => {
+ await services.cleaUp();
+ });
+
+ after(async () => {
+ await services.cleaUp();
+ });
+
+ step('Go to monitors page', async () => {
+ await syntheticsApp.navigateToOverview(true, 15);
+ });
+
+ step('add test monitor', async () => {
+ configId = await services.addTestMonitor(
+ 'Test Monitor',
+ {
+ type: 'http',
+ urls: 'https://www.google.com',
+ locations: ['us_central'],
+ },
+ configId
+ );
+ await services.addTestSummaryDocument({ timestamp: firstCheckTime, configId });
+ });
+
+ step('should create status rule', async () => {
+ await page.getByTestId('syntheticsRefreshButtonButton').click();
+ await page.getByTestId('syntheticsAlertsRulesButton').click();
+ await page.getByTestId('manageStatusRuleName').click();
+ await page.getByTestId('createNewStatusRule').click();
+
+ await page.getByTestId('ruleNameInput').fill('Synthetics status rule');
+ await page.getByTestId('saveRuleButton').click();
+ await page.getByTestId('confirmModalConfirmButton').click();
+
+ await page.waitForSelector(`text='Created rule "Synthetics status rule"'`);
+ });
+
+ step('verify rule creation', async () => {
+ await retry.try(async () => {
+ const rules = await services.getRules();
+ expect(rules.length).toBe(3);
+ expect(rules[2].params).toStrictEqual({
+ condition: {
+ downThreshold: 3,
+ locationsThreshold: 1,
+ groupBy: 'locationId',
+ window: {
+ numberOfChecks: 5,
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts
index 4807255cc28ee..e2285d499a0f2 100644
--- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts
+++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts
@@ -29,20 +29,27 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
before(async () => {
await services.cleaUp();
- await services.enableMonitorManagedViaApi();
+ });
+
+ after(async () => {
+ await services.cleaUp();
+ });
+
+ step('setup monitor', async () => {
+ const connectorId = await services.setupTestConnector();
+ await services.setupSettings(connectorId.id);
+
configId = await services.addTestMonitor('Test Monitor', {
type: 'http',
urls: 'https://www.google.com',
- custom_heartbeat_id: 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
locations: [
{ id: 'us_central', label: 'North America - US Central', isServiceManaged: true },
],
});
- await services.addTestSummaryDocument({ timestamp: firstCheckTime, configId });
- });
-
- after(async () => {
- await services.cleaUp();
+ await services.addTestSummaryDocument({
+ timestamp: firstCheckTime,
+ configId,
+ });
});
step('Go to monitors page', async () => {
@@ -50,21 +57,22 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
});
step('should create default status alert', async () => {
- await page.click(byTestId('xpack.synthetics.alertsPopover.toggleButton'));
- await page.isDisabled(byTestId('xpack.synthetics.toggleAlertFlyout'));
- await page.click(byTestId('xpack.synthetics.toggleAlertFlyout'));
+ await page.getByTestId('syntheticsAlertsRulesButton').click();
+ await page.getByTestId('manageStatusRuleName').click();
+ await page.isDisabled(byTestId('editDefaultStatusRule'));
+ await page.getByTestId('editDefaultStatusRule').click();
+
await page.waitForSelector('text=Monitor status rule');
- expect(await page.locator(`[data-test-subj="intervalFormRow"]`).count()).toEqual(0);
+ await page.getByTestId('intervalInputUnit').selectOption('second');
+ await page.getByTestId('intervalInput').fill('20');
await page.click(byTestId('saveEditedRuleButton'));
await page.waitForSelector("text=Updated 'Synthetics status internal rule'");
});
step('Monitor is as up in overview page', async () => {
await retry.tryForTime(90 * 1000, async () => {
- const totalDown = await page.textContent(
- byTestId('xpack.uptime.synthetics.overview.status.up')
- );
- expect(totalDown).toBe('1Up');
+ const totalUp = await page.textContent(byTestId('syntheticsOverviewUp'));
+ expect(totalUp).toBe('1Up');
});
await page.hover('text=Test Monitor');
@@ -74,6 +82,8 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
step('Disable default alert for monitor', async () => {
await page.click('text=Disable status alert');
await page.waitForSelector(`text=Alerts are now disabled for the monitor "Test Monitor".`);
+ await page.getByTestId('Test Monitor-us_central-metric-item').hover();
+ await page.click('[aria-label="Open actions menu"]');
await page.click('text=Enable status alert');
});
@@ -91,9 +101,7 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
await page.waitForTimeout(5 * 1000);
- const totalDown = await page.textContent(
- byTestId('xpack.uptime.synthetics.overview.status.down')
- );
+ const totalDown = await page.textContent(byTestId('syntheticsOverviewDown'));
expect(totalDown).toBe('1Down');
});
@@ -103,14 +111,17 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
const reasonMessage = getReasonMessage({
name: 'Test Monitor',
location: 'North America - US Central',
- timestamp: downCheckTime,
status: 'down',
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
});
await retry.tryForTime(3 * 60 * 1000, async () => {
await page.click(byTestId('querySubmitButton'));
- const alerts = await page.waitForSelector(`text=1 Alert`, { timeout: 20 * 1000 });
+ const alerts = await page.waitForSelector(`text=1 Alert`, { timeout: 5 * 1000 });
expect(await alerts.isVisible()).toBe(true);
const text = await page.textContent(`${byTestId('dataGridRowCell')} .euiLink`);
@@ -164,8 +175,11 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
const reasonMessage = getReasonMessage({
name,
location: 'North America - US Central',
- timestamp: downCheckTime,
status: 'down',
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
});
await retry.tryForTime(3 * 60 * 1000, async () => {
@@ -194,6 +208,5 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
await page.waitForTimeout(10 * 1000);
await page.click('[aria-label="View in app"]');
- await page.click(byTestId('breadcrumb /app/synthetics/monitors'));
});
});
diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/index.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/index.ts
index a35e62ec4fe6a..1e2b17b34e096 100644
--- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/index.ts
+++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/index.ts
@@ -18,7 +18,8 @@ export * from './private_locations.journey';
export * from './alerting_default.journey';
export * from './global_parameters.journey';
export * from './detail_flyout';
-// export * from './alert_rules/default_status_alert.journey';
+export * from './alert_rules/default_status_alert.journey';
+export * from './alert_rules/custom_status_alert.journey';
export * from './test_now_mode.journey';
export * from './monitor_details_page/monitor_summary.journey';
export * from './test_run_details.journey';
diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts
index d3964692a38f0..af5c66595cda8 100644
--- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts
+++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { DocOverrides } from './sample_docs';
+import { DocOverrides } from '@kbn/observability-synthetics-test-data/src/make_summaries';
export const getGeoData = (locationName?: string, locationId?: string) => ({
observer: {
@@ -22,10 +22,10 @@ export const journeySummary = ({
timestamp,
monitorId,
testRunId,
- locationName,
+ location,
}: DocOverrides = {}) => {
return {
- ...getGeoData(locationName),
+ ...getGeoData(location?.label),
summary: {
up: 1,
down: 0,
@@ -105,9 +105,9 @@ export const journeyStart = ({
timestamp,
monitorId,
testRunId,
- locationName,
+ location,
}: DocOverrides = {}) => ({
- ...getGeoData(locationName),
+ ...getGeoData(location?.label),
test_run_id: testRunId ?? '07e339f4-4d56-4cdb-b314-96faacaee645',
agent: {
name: 'job-88fe737c53c39aea-lp69x',
@@ -167,14 +167,8 @@ export const journeyStart = ({
},
});
-export const step1 = ({
- name,
- timestamp,
- monitorId,
- testRunId,
- locationName,
-}: DocOverrides = {}) => ({
- ...getGeoData(locationName),
+export const step1 = ({ name, timestamp, monitorId, testRunId, location }: DocOverrides = {}) => ({
+ ...getGeoData(location?.label),
test_run_id: testRunId ?? 'c16b1614-7f48-4791-8f46-9ccf3a896e20',
agent: {
name: 'job-76905d93798e6fff-z6nsb',
@@ -249,14 +243,8 @@ export const step1 = ({
},
});
-export const step2 = ({
- name,
- timestamp,
- monitorId,
- testRunId,
- locationName,
-}: DocOverrides = {}) => ({
- ...getGeoData(locationName),
+export const step2 = ({ name, timestamp, monitorId, testRunId, location }: DocOverrides = {}) => ({
+ ...getGeoData(location?.label),
test_run_id: testRunId ?? 'c16b1614-7f48-4791-8f46-9ccf3a896e20',
agent: {
name: 'job-76905d93798e6fff-z6nsb',
diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts
index 028f8a736e93c..23c5ef45d1383 100644
--- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts
+++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts
@@ -9,10 +9,10 @@ import axios from 'axios';
import type { Client } from '@elastic/elasticsearch';
import { KbnClient } from '@kbn/test';
import pMap from 'p-map';
+import { makeDownSummary, makeUpSummary } from '@kbn/observability-synthetics-test-data';
import { SyntheticsMonitor } from '@kbn/synthetics-plugin/common/runtime_types';
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
import { journeyStart, journeySummary, step1, step2 } from './data/browser_docs';
-import { firstDownHit, getUpHit } from './data/sample_docs';
export class SyntheticsServices {
kibanaUrl: string;
@@ -113,22 +113,6 @@ export class SyntheticsServices {
);
}
- async enableDefaultAlertingViaApi() {
- try {
- await axios.post(
- this.kibanaUrl + SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING,
- { isDisabled: false },
- {
- auth: { username: 'elastic', password: 'changeme' },
- headers: { 'kbn-xsrf': 'true', 'x-elastic-internal-origin': 'synthetics-e2e' },
- }
- );
- } catch (e) {
- // eslint-disable-next-line no-console
- console.log(e);
- }
- }
-
async addTestSummaryDocument({
docType = 'summaryUp',
timestamp = new Date(Date.now()).toISOString(),
@@ -157,14 +141,22 @@ export class SyntheticsServices {
let index = 'synthetics-http-default';
- const commonData = { timestamp, monitorId, name, testRunId, locationName, configId };
+ const commonData = {
+ timestamp,
+ name,
+ testRunId,
+ location: {
+ id: 'us_central',
+ label: locationName ?? 'North America - US Central',
+ },
+ configId,
+ monitorId: monitorId ?? configId,
+ };
switch (docType) {
case 'stepEnd':
index = 'synthetics-browser-default';
-
const stepDoc = stepIndex === 1 ? step1(commonData) : step2(commonData);
-
document = { ...stepDoc, ...document };
break;
case 'journeyEnd':
@@ -177,19 +169,19 @@ export class SyntheticsServices {
break;
case 'summaryDown':
document = {
- ...firstDownHit(commonData),
+ ...makeDownSummary(commonData),
...document,
};
break;
case 'summaryUp':
document = {
- ...getUpHit(commonData),
+ ...makeUpSummary(commonData),
...document,
};
break;
default:
document = {
- ...getUpHit(commonData),
+ ...makeUpSummary(commonData),
...document,
};
}
@@ -228,4 +220,43 @@ export class SyntheticsServices {
console.log(e);
}
}
+
+ async getRules() {
+ const response = await axios.get(this.kibanaUrl + '/internal/alerting/rules/_find', {
+ auth: { username: 'elastic', password: 'changeme' },
+ headers: { 'kbn-xsrf': 'true' },
+ });
+ return response.data.data;
+ }
+
+ async setupTestConnector() {
+ const indexConnector = {
+ name: 'test index',
+ config: { index: 'test-index' },
+ secrets: {},
+ connector_type_id: '.index',
+ };
+ const connector = await this.requester.request({
+ path: `/api/actions/connector`,
+ method: 'POST',
+ body: indexConnector,
+ });
+ return connector.data as any;
+ }
+
+ async setupSettings(connectorId?: string) {
+ const settings = {
+ certExpirationThreshold: 30,
+ certAgeThreshold: 730,
+ defaultConnectors: [connectorId],
+ defaultEmail: { to: [], cc: [], bcc: [] },
+ defaultStatusRuleEnabled: true,
+ };
+ const connector = await this.requester.request({
+ path: `/api/synthetics/settings`,
+ method: 'PUT',
+ body: settings,
+ });
+ return connector.data;
+ }
}
diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/tsconfig.json b/x-pack/plugins/observability_solution/synthetics/e2e/tsconfig.json
index bbc7edf10c1f6..7584c000a76fa 100644
--- a/x-pack/plugins/observability_solution/synthetics/e2e/tsconfig.json
+++ b/x-pack/plugins/observability_solution/synthetics/e2e/tsconfig.json
@@ -14,8 +14,9 @@
"@kbn/ftr-common-functional-services",
"@kbn/apm-plugin",
"@kbn/es-archiver",
- "@kbn/repo-info",
"@kbn/synthetics-plugin",
+ "@kbn/repo-info",
+ "@kbn/observability-synthetics-test-data",
"@kbn/ftr-common-functional-ui-services"
]
}
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/condition_locations_value.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/condition_locations_value.tsx
new file mode 100644
index 0000000000000..7b5babfd38786
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/condition_locations_value.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiFieldNumber, EuiPopoverTitle } from '@elastic/eui';
+import { StatusRuleCondition } from '../../../../../../common/rules/status_rule';
+import { PopoverExpression } from './popover_expression';
+import { StatusRuleParamsProps } from '../status_rule_ui';
+
+interface Props {
+ ruleParams: StatusRuleParamsProps['ruleParams'];
+ setRuleParams: StatusRuleParamsProps['setRuleParams'];
+}
+
+export const LocationsValueExpression = ({ ruleParams, setRuleParams }: Props) => {
+ const { condition } = ruleParams;
+
+ const onLocationCountChange = useCallback(
+ (value: number) => {
+ setRuleParams('condition', {
+ ...ruleParams.condition,
+ locationsThreshold: value,
+ groupBy: value === 1 ? ruleParams.condition?.groupBy : 'none',
+ } as StatusRuleCondition);
+ },
+ [ruleParams.condition, setRuleParams]
+ );
+
+ const locationsThreshold =
+ condition && 'locationsThreshold' in condition ? condition.locationsThreshold ?? 1 : 1;
+ return (
+
+
+ {i18n.translate('xpack.synthetics.windowValueExpression.numberOfLocPopoverTitleLabel', {
+ defaultMessage: 'Number of locations',
+ })}
+
+ onLocationCountChange(Number(evt.target.value))}
+ />
+
+ );
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/condition_window_value.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/condition_window_value.tsx
new file mode 100644
index 0000000000000..717a15f9da4f2
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/condition_window_value.tsx
@@ -0,0 +1,107 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ForLastExpression, TIME_UNITS } from '@kbn/triggers-actions-ui-plugin/public';
+import React, { useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiFieldNumber, EuiPopoverTitle } from '@elastic/eui';
+import { PopoverExpression } from './popover_expression';
+import { getConditionType, TimeWindow } from '../../../../../../common/rules/status_rule';
+import { StatusRuleParamsProps } from '../status_rule_ui';
+
+interface Props {
+ ruleParams: StatusRuleParamsProps['ruleParams'];
+ setRuleParams: StatusRuleParamsProps['setRuleParams'];
+}
+
+export const WindowValueExpression = ({ ruleParams, setRuleParams }: Props) => {
+ const { condition } = ruleParams;
+ const timeWindow =
+ condition && 'time' in condition.window
+ ? condition.window.time ?? {
+ size: 5,
+ unit: 'm',
+ }
+ : null;
+
+ const timeWindowSize = timeWindow?.size ?? 5;
+ const timeWindowUnit = timeWindow?.unit ?? 'm';
+
+ const numberOfChecks =
+ condition && 'numberOfChecks' in condition.window ? condition.window.numberOfChecks : null;
+
+ const { useTimeWindow } = getConditionType(ruleParams.condition);
+
+ const onTimeWindowChange = useCallback(
+ (value: TimeWindow) => {
+ setRuleParams('condition', {
+ ...ruleParams.condition,
+ window: {
+ ...ruleParams.condition?.window,
+ time: value,
+ },
+ });
+ },
+ [ruleParams.condition, setRuleParams]
+ );
+
+ const onNumberOfChecksChange = useCallback(
+ (value: number) => {
+ setRuleParams('condition', {
+ ...ruleParams.condition,
+ window: {
+ ...ruleParams.condition?.window,
+ numberOfChecks: value,
+ },
+ });
+ },
+ [ruleParams.condition, setRuleParams]
+ );
+
+ if (!useTimeWindow) {
+ return (
+
+
+ {i18n.translate(
+ 'xpack.synthetics.windowValueExpression.numberOfChecksPopoverTitleLabel',
+ { defaultMessage: 'Number of checks' }
+ )}
+
+ onNumberOfChecksChange(Number(evt.target.value))}
+ />
+
+ );
+ }
+
+ return (
+ {
+ onTimeWindowChange({ size: val ?? 5, unit: timeWindowUnit });
+ }}
+ onChangeWindowUnit={(val) => {
+ onTimeWindowChange({ size: timeWindowSize, unit: (val ?? 'm') as TIME_UNITS });
+ }}
+ errors={{}}
+ description=""
+ />
+ );
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_filters.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_filters.tsx
new file mode 100644
index 0000000000000..31bf9e45ed8f3
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_filters.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import React, { useCallback, useState } from 'react';
+import { useFetchSyntheticsSuggestions } from '../hooks/use_fetch_synthetics_suggestions';
+import { StatusRuleParamsProps } from '../status_rule_ui';
+import { LocationsField, MonitorField, MonitorTypeField, ProjectsField, TagsField } from './fields';
+
+type FieldKeys = 'monitorIds' | 'projects' | 'tags' | 'locations' | 'monitorTypes';
+
+interface Props {
+ ruleParams: StatusRuleParamsProps['ruleParams'];
+ setRuleParams: StatusRuleParamsProps['setRuleParams'];
+}
+
+export const FieldFilters = ({ ruleParams, setRuleParams }: Props) => {
+ const [search, setSearch] = useState('');
+ const [selectedField, setSelectedField] = useState();
+
+ const {
+ suggestions = [],
+ isLoading,
+ allSuggestions,
+ } = useFetchSyntheticsSuggestions({
+ search,
+ fieldName: selectedField,
+ });
+
+ const onFieldChange = useCallback(
+ (key: FieldKeys, value?: string[]) => {
+ setRuleParams(key, value);
+ },
+ [setRuleParams]
+ );
+
+ return (
+ <>
+
+
+ {
+ onFieldChange('monitorIds', val);
+ }}
+ value={ruleParams.monitorIds}
+ setSearch={setSearch}
+ suggestions={suggestions}
+ allSuggestions={allSuggestions}
+ isLoading={isLoading}
+ setSelectedField={setSelectedField}
+ selectedField={selectedField}
+ />
+
+
+ {
+ onFieldChange('monitorTypes', val);
+ }}
+ value={ruleParams.monitorTypes}
+ setSearch={setSearch}
+ suggestions={suggestions}
+ allSuggestions={allSuggestions}
+ isLoading={isLoading}
+ setSelectedField={setSelectedField}
+ selectedField={selectedField}
+ />
+
+
+
+
+
+ {
+ onFieldChange('tags', val);
+ }}
+ value={ruleParams.tags}
+ setSearch={setSearch}
+ suggestions={suggestions}
+ allSuggestions={allSuggestions}
+ isLoading={isLoading}
+ setSelectedField={setSelectedField}
+ selectedField={selectedField}
+ />
+
+
+ {
+ onFieldChange('projects', val);
+ }}
+ value={ruleParams.projects}
+ setSearch={setSearch}
+ suggestions={suggestions}
+ allSuggestions={allSuggestions}
+ isLoading={isLoading}
+ setSelectedField={setSelectedField}
+ selectedField={selectedField}
+ />
+
+
+
+
+
+ {
+ onFieldChange('locations', val);
+ }}
+ value={ruleParams.locations}
+ setSearch={setSearch}
+ suggestions={suggestions}
+ allSuggestions={allSuggestions}
+ isLoading={isLoading}
+ setSelectedField={setSelectedField}
+ selectedField={selectedField}
+ />
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_popover_expression.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_popover_expression.tsx
new file mode 100644
index 0000000000000..c5927e6c0e6b9
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_popover_expression.tsx
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { ReactNode } from 'react';
+import { EuiExpression, EuiPopover, EuiExpressionProps } from '@elastic/eui';
+import { ALL_VALUE } from '@kbn/slo-schema';
+import { isEmpty } from 'lodash';
+import { allOptionText } from './fields';
+import { Suggestion } from '../hooks/use_fetch_synthetics_suggestions';
+
+interface Props {
+ title?: ReactNode;
+ value?: string[];
+ children?: ReactNode;
+ color?: EuiExpressionProps['color'];
+ selectedField?: string;
+ fieldName: string;
+ setSelectedField: (value?: string) => void;
+ allSuggestions?: Record;
+}
+
+export function FieldPopoverExpression({
+ title,
+ value,
+ children,
+ color,
+ selectedField,
+ fieldName,
+ setSelectedField,
+ allSuggestions,
+}: Props) {
+ const isPopoverOpen = selectedField === fieldName;
+
+ const suggestions = allSuggestions?.[fieldName];
+
+ let label =
+ !isEmpty(value) && value
+ ? suggestions
+ ?.filter((suggestion) => value.includes(suggestion.value))
+ ?.map((suggestion) => suggestion.label)
+ .join(', ')
+ : allOptionText;
+
+ if (value?.includes(ALL_VALUE)) {
+ label = allOptionText;
+ }
+
+ const closePopover = () => setSelectedField(selectedField === fieldName ? undefined : fieldName);
+ return (
+
+
+ }
+ repositionOnScroll
+ >
+ {children}
+
+
+ );
+}
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_selector.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_selector.test.tsx
new file mode 100644
index 0000000000000..0255b0014a1f0
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_selector.test.tsx
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { onFieldChange } from './field_selector';
+import { EuiComboBoxOptionOption } from '@elastic/eui';
+import { ALL_VALUE } from '@kbn/slo-schema';
+
+describe('onFieldChange', () => {
+ let onChangeMock: jest.Mock;
+
+ beforeEach(() => {
+ onChangeMock = jest.fn();
+ });
+
+ it('should filter out ALL_VALUE when a specific value is selected', () => {
+ const selected: Array> = [
+ { label: 'Option 2', value: ALL_VALUE },
+ { label: 'Option 1', value: 'value1' },
+ ];
+
+ onFieldChange(selected, onChangeMock);
+
+ expect(onChangeMock).toHaveBeenCalledWith(['value1']);
+ });
+
+ it('should return an empty array when ALL_VALUE is selected', () => {
+ const selected: Array> = [
+ { label: 'Option 1', value: 'value1' },
+ { label: 'Option 2', value: ALL_VALUE },
+ ];
+
+ onFieldChange(selected, onChangeMock);
+
+ expect(onChangeMock).toHaveBeenCalledWith([]);
+ });
+
+ it('should return an empty array when selected is empty', () => {
+ const selected: Array> = [];
+
+ onFieldChange(selected, onChangeMock);
+
+ expect(onChangeMock).toHaveBeenCalledWith([]);
+ });
+
+ it('should call onChange with the filtered array when no ALL_VALUE is present', () => {
+ const selected: Array> = [
+ { label: 'Option 1', value: 'value1' },
+ { label: 'Option 2', value: 'value2' },
+ ];
+
+ onFieldChange(selected, onChangeMock);
+
+ expect(onChangeMock).toHaveBeenCalledWith(['value1', 'value2']);
+ });
+
+ it('should return an empty array if the last selected option is ALL_VALUE', () => {
+ const selected: Array> = [
+ { label: 'Option 1', value: 'value1' },
+ { label: 'Option 2', value: ALL_VALUE },
+ ];
+
+ onFieldChange(selected, onChangeMock);
+
+ expect(onChangeMock).toHaveBeenCalledWith([]);
+ });
+});
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_selector.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_selector.tsx
new file mode 100644
index 0000000000000..96b44e7e5dce4
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/field_selector.tsx
@@ -0,0 +1,110 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
+import { ALL_VALUE } from '@kbn/slo-schema';
+import { debounce } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { Suggestion } from '../hooks/use_fetch_synthetics_suggestions';
+
+interface Option {
+ label: string;
+ value: string;
+}
+
+export interface Props {
+ allowAllOption?: boolean;
+ dataTestSubj: string;
+ fieldName: 'monitorIds' | 'projects' | 'tags' | 'locations' | 'monitorTypes';
+ suggestions?: Suggestion[];
+ isLoading?: boolean;
+ required?: boolean;
+ value?: string[];
+ onChange: (selected: string[]) => void;
+ placeholder: string;
+ setSearch: (val: string) => void;
+ setSelectedField: (value: string) => void;
+}
+
+const ALL_OPTION = {
+ label: i18n.translate('xpack.synthetics.filter.alert.allLabel', {
+ defaultMessage: 'All',
+ }),
+ value: ALL_VALUE,
+};
+
+export function FieldSelector({
+ allowAllOption = true,
+ dataTestSubj,
+ value,
+ onChange,
+ isLoading,
+ placeholder,
+ suggestions,
+ setSearch,
+}: Props) {
+ const options = (allowAllOption ? [ALL_OPTION] : []).concat(createOptions(suggestions));
+
+ const debouncedSearch = debounce((val) => setSearch(val), 200);
+
+ return (
+
+ >) => {
+ onFieldChange(selected, onChange);
+ }}
+ onSearchChange={(val: string) => debouncedSearch(val)}
+ options={options}
+ selectedOptions={value?.map((val) => {
+ const option = options.find((opt) => opt.value === val);
+ if (option) {
+ return {
+ value: val,
+ label: option.label,
+ 'data-test-subj': `${dataTestSubj}SelectedValue`,
+ };
+ }
+ return {
+ value: val,
+ label: val,
+ 'data-test-subj': `${dataTestSubj}SelectedValue`,
+ };
+ })}
+ />
+
+ );
+}
+
+export const onFieldChange = (
+ selected: Array>,
+ onChange: (selected: string[]) => void
+) => {
+ // removes ALL value option if a specific value is selected
+ if (selected.length && selected.at(-1)?.value !== ALL_VALUE) {
+ onChange(selected.filter((val) => val.value !== ALL_VALUE).map((val) => val.value!));
+ return;
+ }
+ // removes specific value if ALL value is selected
+ if (selected.length && selected.at(-1)?.value === ALL_VALUE) {
+ onChange([]);
+ return;
+ }
+
+ onChange([]);
+};
+
+function createOptions(suggestions: Suggestion[] = []): Option[] {
+ return suggestions
+ .map((suggestion) => ({ label: suggestion.label, value: suggestion.value }))
+ .sort((a, b) => String(a.label).localeCompare(b.label));
+}
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/fields.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/fields.tsx
new file mode 100644
index 0000000000000..2c2e9714d998e
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/fields.tsx
@@ -0,0 +1,159 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { FieldPopoverExpression } from './field_popover_expression';
+import { Suggestion } from '../hooks/use_fetch_synthetics_suggestions';
+import { FieldSelector } from './field_selector';
+
+interface FieldProps {
+ value?: string[];
+ onChange: (value?: string[]) => void;
+ setSearch: (val: string) => void;
+ suggestions?: Suggestion[];
+ allSuggestions?: Record;
+ isLoading?: boolean;
+ setSelectedField: (value?: string) => void;
+ selectedField?: string;
+}
+
+export const allOptionText = i18n.translate('xpack.synthetics.filter.alert.allLabel', {
+ defaultMessage: 'All',
+});
+
+export function MonitorField({ value, onChange, ...rest }: FieldProps) {
+ return (
+
+
+
+ );
+}
+
+export function TagsField({ value, onChange, ...rest }: FieldProps) {
+ return (
+
+
+
+ );
+}
+
+export function MonitorTypeField({ value, onChange, ...rest }: FieldProps) {
+ const label = i18n.translate('xpack.synthetics.alerting.fields.type', {
+ defaultMessage: 'Type',
+ });
+ return (
+
+
+
+ );
+}
+
+export function LocationsField({ value, onChange, ...rest }: FieldProps) {
+ const label = i18n.translate('xpack.synthetics.alerting.fields.location', {
+ defaultMessage: 'Locations',
+ });
+ return (
+
+
+
+ );
+}
+
+export function ProjectsField({ value, onChange, ...rest }: FieldProps) {
+ const label = i18n.translate('xpack.synthetics.alerting.fields.project', {
+ defaultMessage: 'Projects',
+ });
+ return (
+
+
+
+ );
+}
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/for_the_last_expression.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/for_the_last_expression.tsx
new file mode 100644
index 0000000000000..81153d88be61d
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/for_the_last_expression.tsx
@@ -0,0 +1,172 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { EuiExpression, EuiPopover, EuiPopoverTitle, EuiSelectable } from '@elastic/eui';
+import React, { useEffect, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { getConditionType, StatusRuleCondition } from '../../../../../../common/rules/status_rule';
+import { StatusRuleParamsProps } from '../status_rule_ui';
+
+interface Props {
+ ruleParams: StatusRuleParamsProps['ruleParams'];
+ setRuleParams: StatusRuleParamsProps['setRuleParams'];
+}
+
+export const WITHIN_TOTAL_CHECKS_LABEL = i18n.translate(
+ 'xpack.synthetics.monitorStatusRule.withinTotalChecks.label',
+ {
+ defaultMessage: 'Within total checks',
+ }
+);
+
+export const WITHIN_TOTAL_CHECKS_EXPRESSION = i18n.translate(
+ 'xpack.synthetics.monitorStatusRule.withinTotalChecks.expression',
+ {
+ defaultMessage: 'Within the last',
+ }
+);
+
+export const WITHIN_TIMERANGE_EXPRESSION = i18n.translate(
+ 'xpack.synthetics.monitorStatusRule.withinTimerange.expression',
+ {
+ defaultMessage: 'Within the last',
+ }
+);
+
+export const WITHIN_TIMERANGE_LABEL = i18n.translate(
+ 'xpack.synthetics.monitorStatusRule.withinTimerange.label',
+ {
+ defaultMessage: 'Within timerange',
+ }
+);
+
+interface Option {
+ label: string;
+ key: 'checksWindow' | 'timeWindow' | 'locations';
+}
+
+const OPTIONS: Option[] = [
+ {
+ label: WITHIN_TOTAL_CHECKS_LABEL,
+ key: 'checksWindow',
+ },
+ {
+ label: WITHIN_TIMERANGE_LABEL,
+ key: 'timeWindow',
+ },
+];
+
+export const DEFAULT_CONDITION = {
+ window: { numberOfChecks: 5 },
+ groupBy: 'locationId',
+ downThreshold: 3,
+ locationsThreshold: 1,
+};
+const getCheckedOption = (option: Option, condition?: StatusRuleCondition) => {
+ const { useTimeWindow, isLocationBased } = getConditionType(condition);
+
+ if (isLocationBased && option.key === 'locations') {
+ return 'on';
+ }
+
+ if (option.key === 'timeWindow' && useTimeWindow && !isLocationBased) {
+ return 'on';
+ }
+ if (option.key === 'checksWindow' && !useTimeWindow && !isLocationBased) {
+ return 'on';
+ }
+
+ return undefined;
+};
+
+export const ForTheLastExpression = ({ ruleParams, setRuleParams }: Props) => {
+ const { condition } = ruleParams;
+
+ const { useTimeWindow } = getConditionType(condition);
+
+ const [isOpen, setIsOpen] = useState(false);
+
+ const [options, setOptions] = useState(OPTIONS);
+
+ useEffect(() => {
+ if (!condition) {
+ setRuleParams('condition', DEFAULT_CONDITION);
+ }
+ }, [condition, setRuleParams]);
+
+ useEffect(() => {
+ setOptions(
+ OPTIONS.map((option) => ({
+ key: option.key as 'checksWindow' | 'timeWindow',
+ label: option.label,
+ checked: getCheckedOption(option, condition),
+ }))
+ );
+ }, [condition, useTimeWindow]);
+
+ const getDescriptiveText = () => {
+ if (useTimeWindow) {
+ return WITHIN_TIMERANGE_EXPRESSION;
+ }
+ return WITHIN_TOTAL_CHECKS_EXPRESSION;
+ };
+
+ return (
+ setIsOpen(!isOpen)}
+ />
+ }
+ isOpen={isOpen}
+ closePopover={() => setIsOpen(false)}
+ anchorPosition="downLeft"
+ >
+
+ singleSelection="always"
+ options={options}
+ onChange={(selectedValues) => {
+ const selectedValue = selectedValues.filter((v) => v.checked === 'on')?.[0];
+ switch (selectedValue?.key) {
+ case 'checksWindow':
+ setRuleParams('condition', {
+ ...ruleParams.condition,
+ downThreshold: 5,
+ locationsThreshold: 1,
+ window: { numberOfChecks: 5 },
+ });
+ break;
+ case 'timeWindow':
+ setRuleParams('condition', {
+ ...ruleParams.condition,
+ downThreshold: 5,
+ locationsThreshold: 1,
+ window: { time: { unit: 'm', size: 5 } },
+ });
+ break;
+ default:
+ break;
+ }
+ }}
+ >
+ {(list) => (
+
+
+ {i18n.translate('xpack.synthetics.forTheLastExpression.whenPopoverTitleLabel', {
+ defaultMessage: 'When',
+ })}
+
+ {list}
+
+ )}
+
+
+ );
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/group_by_field.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/group_by_field.tsx
new file mode 100644
index 0000000000000..92cc2abe3517c
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/group_by_field.tsx
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiSwitch } from '@elastic/eui';
+
+export const GroupByExpression = ({
+ onChange,
+ groupByLocation,
+ locationsThreshold,
+}: {
+ locationsThreshold: number;
+ groupByLocation: boolean;
+ onChange: (val: boolean) => void;
+}) => {
+ const disabledGroupBy = locationsThreshold > 1;
+
+ return (
+
+
+ onChange(e.target.checked)}
+ />
+
+
+ {disabledGroupBy ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/popover_expression.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/popover_expression.tsx
new file mode 100644
index 0000000000000..3841f25ac2a8b
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/common/popover_expression.tsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState, ReactNode } from 'react';
+import { EuiExpression, EuiPopover, EuiExpressionProps } from '@elastic/eui';
+
+interface Props {
+ title?: ReactNode;
+ value: ReactNode;
+ children?: ReactNode;
+ color?: EuiExpressionProps['color'];
+}
+
+export function PopoverExpression(props: Props) {
+ const { title, value, children, color } = props;
+ const [popoverOpen, setPopoverOpen] = useState(false);
+
+ return (
+ setPopoverOpen(false)}
+ button={
+ setPopoverOpen((state) => !state)}
+ />
+ }
+ repositionOnScroll
+ >
+ {children}
+
+ );
+}
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/hooks/translations.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/hooks/translations.ts
index 2901b67820485..eb54c108b5de3 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/hooks/translations.ts
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/hooks/translations.ts
@@ -21,12 +21,6 @@ export const ToggleFlyoutTranslations = {
toggleTlsAriaLabel: i18n.translate('xpack.synthetics.toggleAlertFlyout.tls.ariaLabel', {
defaultMessage: 'Open add tls rule flyout',
}),
- toggleMonitorStatusContent: i18n.translate('xpack.synthetics.toggleAlertButton.content', {
- defaultMessage: 'Monitor status rule',
- }),
- toggleTlsContent: i18n.translate('xpack.synthetics.toggleTlsAlertButton.label.content', {
- defaultMessage: 'TLS certificate rule',
- }),
navigateToAlertingUIAriaLabel: i18n.translate('xpack.synthetics.app.navigateToAlertingUi', {
defaultMessage: 'Leave Synthetics and go to Alerting Management page',
}),
@@ -40,3 +34,11 @@ export const ToggleFlyoutTranslations = {
defaultMessage: 'Alerts and rules',
}),
};
+
+export const TLS_RULE_NAME = i18n.translate('xpack.synthetics.toggleTlsAlertButton.label.content', {
+ defaultMessage: 'TLS certificate rule',
+});
+
+export const STATUS_RULE_NAME = i18n.translate('xpack.synthetics.toggleAlertButton.content', {
+ defaultMessage: 'Monitor status rule',
+});
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/hooks/use_fetch_synthetics_suggestions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/hooks/use_fetch_synthetics_suggestions.ts
new file mode 100644
index 0000000000000..a5f16ffab8b7a
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/hooks/use_fetch_synthetics_suggestions.ts
@@ -0,0 +1,66 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { useFetcher } from '@kbn/observability-shared-plugin/public';
+import { ClientPluginsStart } from '../../../../../plugin';
+
+export interface Suggestion {
+ label: string;
+ value: string;
+ count: number;
+}
+
+export interface UseFetchSyntheticsSuggestions {
+ suggestions: Suggestion[];
+ isLoading: boolean;
+ allSuggestions?: Record;
+}
+
+export interface Params {
+ fieldName?: string;
+ filters?: {
+ locations?: string[];
+ monitorIds?: string[];
+ tags?: string[];
+ projects?: string[];
+ };
+ search: string;
+}
+
+type ApiResponse = Record;
+
+export function useFetchSyntheticsSuggestions({
+ filters,
+ fieldName,
+ search,
+}: Params): UseFetchSyntheticsSuggestions {
+ const { http } = useKibana().services;
+ const { locations, monitorIds, tags, projects } = filters || {};
+
+ const { loading, data } = useFetcher(
+ async ({ signal }) => {
+ return await http.get('/internal/synthetics/suggestions', {
+ query: {
+ locations: locations || [],
+ monitorQueryIds: monitorIds || [],
+ tags: tags || [],
+ projects: projects || [],
+ query: search,
+ },
+ signal,
+ });
+ },
+ [http, locations, monitorIds, tags, projects, search]
+ );
+
+ return {
+ suggestions: fieldName ? data?.[fieldName] ?? [] : [],
+ allSuggestions: data,
+ isLoading: Boolean(loading),
+ };
+}
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_rules.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_rules.ts
index 6e03b69b5d60c..5ffb17b639768 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_rules.ts
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_rules.ts
@@ -8,6 +8,8 @@
import { useDispatch, useSelector } from 'react-redux';
import { useCallback, useEffect, useMemo } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { i18n } from '@kbn/i18n';
+import { selectDynamicSettings } from '../../../state/settings';
import { useSyntheticsSettingsContext } from '../../../contexts';
import {
selectSyntheticsAlerts,
@@ -20,6 +22,7 @@ import {
import { SYNTHETICS_TLS_RULE } from '../../../../../../common/constants/synthetics_alerts';
import {
selectAlertFlyoutVisibility,
+ selectIsNewRule,
selectMonitorListState,
setAlertFlyoutVisible,
} from '../../../state';
@@ -31,12 +34,16 @@ export const useSyntheticsRules = (isOpen: boolean) => {
const defaultRules = useSelector(selectSyntheticsAlerts);
const loading = useSelector(selectSyntheticsAlertsLoading);
const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
+ const isNewRule = useSelector(selectIsNewRule);
+ const { settings } = useSelector(selectDynamicSettings);
const { canSave } = useSyntheticsSettingsContext();
const { loaded, data: monitors } = useSelector(selectMonitorListState);
const hasMonitors = loaded && monitors.absoluteTotal && monitors.absoluteTotal > 0;
+ const defaultRulesEnabled =
+ settings && (settings?.defaultStatusRuleEnabled || settings?.defaultTLSRuleEnabled);
const getOrCreateAlerts = useCallback(() => {
if (canSave) {
@@ -47,7 +54,7 @@ export const useSyntheticsRules = (isOpen: boolean) => {
}, [canSave, dispatch]);
useEffect(() => {
- if (hasMonitors) {
+ if (hasMonitors && defaultRulesEnabled) {
if (!defaultRules) {
// on initial load we prioritize loading the app
setTimeout(() => {
@@ -59,22 +66,52 @@ export const useSyntheticsRules = (isOpen: boolean) => {
}
// we don't want to run this on defaultRules change
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [dispatch, isOpen, hasMonitors]);
+ }, [dispatch, isOpen, hasMonitors, defaultRulesEnabled]);
const { triggersActionsUi } = useKibana().services;
const EditAlertFlyout = useMemo(() => {
const initialRule =
alertFlyoutVisible === SYNTHETICS_TLS_RULE ? defaultRules?.tlsRule : defaultRules?.statusRule;
- if (!initialRule) {
+ if (!initialRule || isNewRule) {
return null;
}
return triggersActionsUi.getEditRuleFlyout({
onClose: () => dispatch(setAlertFlyoutVisible(null)),
- hideInterval: true,
initialRule,
});
- }, [defaultRules, dispatch, triggersActionsUi, alertFlyoutVisible]);
+ }, [
+ alertFlyoutVisible,
+ defaultRules?.tlsRule,
+ defaultRules?.statusRule,
+ isNewRule,
+ triggersActionsUi,
+ dispatch,
+ ]);
- return useMemo(() => ({ loading, EditAlertFlyout }), [EditAlertFlyout, loading]);
+ const NewRuleFlyout = useMemo(() => {
+ if (!isNewRule || !alertFlyoutVisible) {
+ return null;
+ }
+ return triggersActionsUi.getAddRuleFlyout({
+ consumer: 'uptime',
+ ruleTypeId: alertFlyoutVisible,
+ onClose: () => dispatch(setAlertFlyoutVisible(null)),
+ initialValues: {
+ name:
+ alertFlyoutVisible === SYNTHETICS_TLS_RULE
+ ? i18n.translate('xpack.synthetics.alerting.defaultRuleName.tls', {
+ defaultMessage: 'Synthetics monitor TLS rule',
+ })
+ : i18n.translate('xpack.synthetics.alerting.defaultRuleName', {
+ defaultMessage: 'Synthetics monitor status rule',
+ }),
+ },
+ });
+ }, [isNewRule, triggersActionsUi, dispatch, alertFlyoutVisible]);
+
+ return useMemo(
+ () => ({ loading, EditAlertFlyout, NewRuleFlyout }),
+ [EditAlertFlyout, loading, NewRuleFlyout]
+ );
};
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/query_bar.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/query_bar.tsx
new file mode 100644
index 0000000000000..11d17abb6c81a
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/query_bar.tsx
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect } from 'react';
+import { i18n } from '@kbn/i18n';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { Filter } from '@kbn/es-query';
+import { EuiFormRow } from '@elastic/eui';
+import { useSyntheticsDataView } from '../../contexts/synthetics_data_view_context';
+import { ClientPluginsStart } from '../../../../plugin';
+
+export function AlertSearchBar({
+ kqlQuery,
+ onChange,
+}: {
+ kqlQuery: string;
+ onChange: (val: { kqlQuery?: string; filters?: Filter[] }) => void;
+}) {
+ const {
+ data: { query },
+ unifiedSearch: {
+ ui: { QueryStringInput },
+ },
+ } = useKibana().services;
+
+ const dataView = useSyntheticsDataView();
+
+ useEffect(() => {
+ const sub = query.state$.subscribe(() => {
+ const queryState = query.getState();
+ onChange({
+ kqlQuery: String(queryState.query),
+ });
+ });
+
+ return sub.unsubscribe;
+ }, [onChange, query]);
+
+ return (
+
+ {
+ onChange({
+ kqlQuery: String(queryN.query),
+ });
+ }}
+ onSubmit={(queryN) => {
+ if (queryN) {
+ onChange({
+ kqlQuery: String(queryN.query),
+ });
+ }
+ }}
+ query={{ query: String(kqlQuery), language: 'kuery' }}
+ autoSubmit={true}
+ disableLanguageSwitcher={true}
+ />
+
+ );
+}
+
+const PLACEHOLDER = i18n.translate('xpack.synthetics.list.search', {
+ defaultMessage: 'Filter by KQL query',
+});
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/rule_name_with_loading.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/rule_name_with_loading.tsx
new file mode 100644
index 0000000000000..4f4b8b5d60ddf
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/rule_name_with_loading.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
+import React from 'react';
+
+export const RuleNameWithLoading = ({
+ ruleName,
+ isLoading,
+}: {
+ ruleName: string;
+ isLoading: boolean;
+}) => {
+ return (
+
+ {ruleName}
+ {isLoading && (
+
+
+
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/status_rule_expression.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/status_rule_expression.tsx
new file mode 100644
index 0000000000000..b3a701d474802
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/status_rule_expression.tsx
@@ -0,0 +1,157 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ EuiExpression,
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiSpacer,
+ EuiTitle,
+ EuiHorizontalRule,
+ EuiIconTip,
+} from '@elastic/eui';
+import React, { useCallback } from 'react';
+import { ValueExpression } from '@kbn/triggers-actions-ui-plugin/public';
+import { i18n } from '@kbn/i18n';
+import { GroupByExpression } from './common/group_by_field';
+import { WindowValueExpression } from './common/condition_window_value';
+import { DEFAULT_CONDITION, ForTheLastExpression } from './common/for_the_last_expression';
+import { StatusRuleParamsProps } from './status_rule_ui';
+import { LocationsValueExpression } from './common/condition_locations_value';
+
+interface Props {
+ ruleParams: StatusRuleParamsProps['ruleParams'];
+ setRuleParams: StatusRuleParamsProps['setRuleParams'];
+}
+
+export const StatusRuleExpression: React.FC = ({ ruleParams, setRuleParams }) => {
+ const condition = ruleParams.condition ?? DEFAULT_CONDITION;
+ const downThreshold = condition?.downThreshold ?? DEFAULT_CONDITION.downThreshold;
+
+ const locationsThreshold = condition?.locationsThreshold ?? DEFAULT_CONDITION.locationsThreshold;
+
+ const onThresholdChange = useCallback(
+ (value: number) => {
+ const prevCondition = ruleParams.condition ?? DEFAULT_CONDITION;
+ setRuleParams('condition', {
+ ...prevCondition,
+ downThreshold: value,
+ });
+ },
+ [ruleParams.condition, setRuleParams]
+ );
+
+ const onGroupByChange = useCallback(
+ (groupByLocation: boolean) => {
+ setRuleParams('condition', {
+ ...(ruleParams?.condition ?? DEFAULT_CONDITION),
+ groupBy: groupByLocation ? 'locationId' : 'none',
+ });
+ },
+ [ruleParams?.condition, setRuleParams]
+ );
+
+ return (
+ <>
+
+
+
+
+
+ {i18n.translate('xpack.synthetics.rules.status.condition.title', {
+ defaultMessage: 'Condition',
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ onThresholdChange(val);
+ }}
+ description={StatusTranslations.isDownDescription}
+ errors={[]}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export const StatusTranslations = {
+ criteriaAriaLabel: i18n.translate('xpack.synthetics.rules.status.criteriaExpression.ariaLabel', {
+ defaultMessage:
+ 'An expression displaying the criteria for the monitors that are being watched by this alert',
+ }),
+ criteriaDescription: i18n.translate(
+ 'xpack.synthetics.alerts.tls.criteriaExpression.description',
+ {
+ defaultMessage: 'when',
+ }
+ ),
+ criteriaValue: i18n.translate('xpack.synthetics.status.criteriaExpression.value', {
+ defaultMessage: 'monitor',
+ }),
+ isDownDescription: i18n.translate('xpack.synthetics.status.expirationExpression.description', {
+ defaultMessage: 'is down ',
+ }),
+ fromLocationsDescription: i18n.translate(
+ 'xpack.synthetics.status.locationsThreshold.description',
+ {
+ defaultMessage: 'from at least',
+ }
+ ),
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/status_rule_ui.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/status_rule_ui.tsx
new file mode 100644
index 0000000000000..70278c8951773
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/status_rule_ui.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback } from 'react';
+import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
+import { Filter } from '@kbn/es-query';
+import { EuiSpacer } from '@elastic/eui';
+import { FieldFilters } from './common/field_filters';
+import { AlertSearchBar } from './query_bar';
+import { StatusRuleExpression } from './status_rule_expression';
+import { StatusRuleParams } from '../../../../../common/rules/status_rule';
+
+export type StatusRuleParamsProps = RuleTypeParamsExpressionProps;
+
+export const StatusRuleComponent: React.FC<{
+ ruleParams: StatusRuleParamsProps['ruleParams'];
+ setRuleParams: StatusRuleParamsProps['setRuleParams'];
+}> = ({ ruleParams, setRuleParams }) => {
+ const onFiltersChange = useCallback(
+ (val: { kqlQuery?: string; filters?: Filter[] }) => {
+ setRuleParams('kqlQuery', val.kqlQuery);
+ },
+ [setRuleParams]
+ );
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/toggle_alert_flyout_button.tsx
index 0a8e5abf37f1a..6203652578480 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/toggle_alert_flyout_button.tsx
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/alerts/toggle_alert_flyout_button.tsx
@@ -11,21 +11,18 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
EuiContextMenu,
EuiContextMenuPanelDescriptor,
- EuiContextMenuPanelItemDescriptor,
- EuiFlexGroup,
- EuiFlexItem,
EuiHeaderLink,
- EuiLoadingSpinner,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { RuleNameWithLoading } from './rule_name_with_loading';
import {
SYNTHETICS_STATUS_RULE,
SYNTHETICS_TLS_RULE,
} from '../../../../../common/constants/synthetics_alerts';
import { ManageRulesLink } from '../common/links/manage_rules_link';
import { ClientPluginsStart } from '../../../../plugin';
-import { ToggleFlyoutTranslations } from './hooks/translations';
+import { STATUS_RULE_NAME, TLS_RULE_NAME, ToggleFlyoutTranslations } from './hooks/translations';
import { useSyntheticsRules } from './hooks/use_synthetics_rules';
import {
selectAlertFlyoutVisibility,
@@ -40,67 +37,84 @@ export const ToggleAlertFlyoutButton = () => {
const { application } = useKibana().services;
const hasUptimeWrite = application?.capabilities.uptime?.save ?? false;
- const { EditAlertFlyout, loading } = useSyntheticsRules(isOpen);
-
+ const { EditAlertFlyout, loading, NewRuleFlyout } = useSyntheticsRules(isOpen);
const { loaded, data: monitors } = useSelector(selectMonitorListState);
const hasMonitors = loaded && monitors.absoluteTotal && monitors.absoluteTotal > 0;
- const monitorStatusAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
- 'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
- 'data-test-subj': 'xpack.synthetics.toggleAlertFlyout',
- name: (
-
- {ToggleFlyoutTranslations.toggleMonitorStatusContent}
- {loading && (
-
-
-
- )}
-
- ),
- onClick: () => {
- dispatch(setAlertFlyoutVisible(SYNTHETICS_STATUS_RULE));
- setIsOpen(false);
- },
- toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
- disabled: !hasUptimeWrite || loading,
- icon: 'bell',
- };
-
- const tlsAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
- 'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
- 'data-test-subj': 'xpack.synthetics.toggleAlertFlyout.tls',
- name: (
-
- {ToggleFlyoutTranslations.toggleTlsContent}
- {loading && (
-
-
-
- )}
-
- ),
- onClick: () => {
- dispatch(setAlertFlyoutVisible(SYNTHETICS_TLS_RULE));
- setIsOpen(false);
- },
- toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
- disabled: !hasUptimeWrite || loading,
- icon: 'bell',
- };
-
- const managementContextItem: EuiContextMenuPanelItemDescriptor = {
- 'aria-label': ToggleFlyoutTranslations.navigateToAlertingUIAriaLabel,
- 'data-test-subj': 'xpack.synthetics.navigateToAlertingUi',
- name: ,
- icon: 'tableOfContents',
- };
-
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
- items: [monitorStatusAlertContextMenuItem, tlsAlertContextMenuItem, managementContextItem],
+ items: [
+ {
+ name: STATUS_RULE_NAME,
+ 'data-test-subj': 'manageStatusRuleName',
+ panel: 1,
+ },
+ {
+ name: TLS_RULE_NAME,
+ 'data-test-subj': 'manageTlsRuleName',
+ panel: 2,
+ },
+ {
+ 'aria-label': ToggleFlyoutTranslations.navigateToAlertingUIAriaLabel,
+ 'data-test-subj': 'xpack.synthetics.navigateToAlertingUi',
+ name: ,
+ icon: 'tableOfContents',
+ },
+ ],
+ },
+ {
+ id: 1,
+ items: [
+ {
+ name: CREATE_STATUS_RULE,
+ 'data-test-subj': 'createNewStatusRule',
+ icon: 'plusInCircle',
+ onClick: () => {
+ dispatch(setAlertFlyoutVisible({ id: SYNTHETICS_STATUS_RULE, isNewRuleFlyout: true }));
+ setIsOpen(false);
+ },
+ },
+ {
+ 'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
+ 'data-test-subj': 'editDefaultStatusRule',
+ name: ,
+ onClick: () => {
+ dispatch(setAlertFlyoutVisible({ id: SYNTHETICS_STATUS_RULE, isNewRuleFlyout: false }));
+ setIsOpen(false);
+ },
+ toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
+ disabled: !hasUptimeWrite || loading,
+ icon: 'bell',
+ },
+ ],
+ },
+ {
+ id: 2,
+ items: [
+ {
+ name: CREATE_TLS_RULE_NAME,
+ 'data-test-subj': 'createNewTLSRule',
+ icon: 'plusInCircle',
+ onClick: () => {
+ dispatch(setAlertFlyoutVisible({ id: SYNTHETICS_TLS_RULE, isNewRuleFlyout: true }));
+ setIsOpen(false);
+ },
+ },
+ {
+ 'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
+ 'data-test-subj': 'editDefaultTlsRule',
+ name: ,
+ onClick: () => {
+ dispatch(setAlertFlyoutVisible({ id: SYNTHETICS_TLS_RULE, isNewRuleFlyout: false }));
+ setIsOpen(false);
+ },
+ toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
+ disabled: !hasUptimeWrite || loading,
+ icon: 'bell',
+ },
+ ],
},
];
@@ -113,7 +127,7 @@ export const ToggleAlertFlyoutButton = () => {
setIsOpen(!isOpen)}
@@ -130,6 +144,7 @@ export const ToggleAlertFlyoutButton = () => {
{alertFlyoutVisible && EditAlertFlyout}
+ {alertFlyoutVisible && NewRuleFlyout}
>
);
};
@@ -140,3 +155,31 @@ const noWritePermissionsTooltipContent = i18n.translate(
defaultMessage: 'You do not have sufficient permissions to perform this action.',
}
);
+
+export const EDIT_TLS_RULE_NAME = i18n.translate(
+ 'xpack.synthetics.toggleTlsAlertButton.label.default',
+ {
+ defaultMessage: 'Edit default TLS rule',
+ }
+);
+
+export const EDIT_STATUS_RULE = i18n.translate(
+ 'xpack.synthetics.toggleStatusAlertButton.label.default',
+ {
+ defaultMessage: 'Edit default status rule',
+ }
+);
+
+export const CREATE_TLS_RULE_NAME = i18n.translate(
+ 'xpack.synthetics.toggleTlsAlertButton.createRule',
+ {
+ defaultMessage: 'Create TLS rule',
+ }
+);
+
+export const CREATE_STATUS_RULE = i18n.translate(
+ 'xpack.synthetics.toggleStatusAlertButton.createRule',
+ {
+ defaultMessage: 'Create status rule',
+ }
+);
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx
index a575ecc110eb4..49098f8de0225 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx
@@ -35,6 +35,10 @@ describe('Monitor Detail Flyout', () => {
status: 'up',
type: 'http',
check_group: 'check-group',
+ timespan: {
+ gte: 'now-15m',
+ lt: 'now',
+ },
},
url: {
full: 'https://www.elastic.co',
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx
index 4f089b2464ed9..88133c5c06d38 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx
@@ -97,7 +97,7 @@ export function OverviewStatus({ titleAppend }: { titleAppend?: React.ReactNode
-
-
-
+ {params.id && isEmpty(ruleParams) && (
+
+
+
+ )}
+
+ {(!params.id || !isEmpty(ruleParams)) && (
+
+ )}
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/monitor_status.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/monitor_status.tsx
index 5f63d6ac298c7..794747853642c 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/monitor_status.tsx
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/monitor_status.tsx
@@ -41,7 +41,7 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({
},
defaultActionMessage,
defaultRecoveryMessage,
- requiresAppContext: true,
+ requiresAppContext: false,
format: ({ fields }) => {
return {
reason: fields[ALERT_REASON] || '',
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/effects.ts
index bbec528db6de2..f1c949ccf2b31 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/effects.ts
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/effects.ts
@@ -7,7 +7,7 @@
import { PayloadAction } from '@reduxjs/toolkit';
import { call, put, takeEvery, select, takeLatest, debounce } from 'redux-saga/effects';
-import { quietFetchOverviewStatusAction } from '../overview_status';
+import { fetchOverviewStatusAction, quietFetchOverviewStatusAction } from '../overview_status';
import { enableDefaultAlertingAction } from '../alert_rules';
import {
ConfigKey,
@@ -15,7 +15,7 @@ import {
SyntheticsMonitorWithId,
} from '../../../../../common/runtime_types';
import { kibanaService } from '../../../../utils/kibana_service';
-import { MonitorOverviewPageState } from '../overview';
+import { MonitorOverviewPageState, selectOverviewPageState } from '../overview';
import { selectOverviewState } from '../overview/selectors';
import { fetchEffectFactory, sendErrorToast, sendSuccessToast } from '../utils/fetch_effect';
import { serializeHttpFetchError } from '../utils/http_error';
@@ -53,7 +53,13 @@ export function* enableMonitorAlertEffect() {
try {
const response = yield call(fetchUpsertMonitor, action.payload);
yield put(enableMonitorAlertAction.success(response as SyntheticsMonitorWithId));
+ const pageState = (yield select(selectOverviewPageState)) as MonitorOverviewPageState;
sendSuccessToast(action.payload.success);
+ yield put(
+ fetchOverviewStatusAction.get({
+ pageState,
+ })
+ );
if (
(response as EncryptedSyntheticsSavedMonitor)[ConfigKey.ALERT_CONFIG]?.status?.enabled
) {
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview_status/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview_status/index.ts
index 6cdadd03d15bd..2670aa913d61a 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview_status/index.ts
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview_status/index.ts
@@ -7,13 +7,7 @@
import { createReducer } from '@reduxjs/toolkit';
-import { enableMonitorAlertAction } from '../monitor_list/actions';
-import { isStatusEnabled } from '../../../../../common/runtime_types/monitor_management/alert_config';
-import {
- ConfigKey,
- OverviewStatusMetaData,
- OverviewStatusState,
-} from '../../../../../common/runtime_types';
+import { OverviewStatusMetaData, OverviewStatusState } from '../../../../../common/runtime_types';
import { IHttpSerializedFetchError } from '..';
import {
clearOverviewStatusErrorAction,
@@ -27,7 +21,6 @@ export interface OverviewStatusStateReducer {
status: OverviewStatusState | null;
allConfigs?: OverviewStatusMetaData[];
disabledConfigs?: OverviewStatusMetaData[];
- sortedByStatus?: OverviewStatusMetaData[];
error: IHttpSerializedFetchError | null;
}
@@ -63,24 +56,6 @@ export const overviewStatusReducer = createReducer(initialState, (builder) => {
state.error = action.payload;
state.loading = false;
})
- .addCase(enableMonitorAlertAction.success, (state, action) => {
- const monitorObject = action.payload;
- if (!('errors' in monitorObject)) {
- const isStatusAlertEnabled = isStatusEnabled(monitorObject[ConfigKey.ALERT_CONFIG]);
- state.allConfigs = state.allConfigs?.map((monitor) => {
- if (
- monitor.configId === monitorObject[ConfigKey.CONFIG_ID] ||
- monitor.monitorQueryId === monitorObject[ConfigKey.MONITOR_QUERY_ID]
- ) {
- return {
- ...monitor,
- isStatusAlertEnabled,
- };
- }
- return monitor;
- });
- }
- })
.addCase(clearOverviewStatusErrorAction, (state) => {
state.error = null;
});
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/actions.ts
index 06b9506ead191..7a9e3e2884b9a 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/actions.ts
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/actions.ts
@@ -16,9 +16,10 @@ export interface PopoverState {
open: boolean;
}
-export const setAlertFlyoutVisible = createAction<
- typeof SYNTHETICS_STATUS_RULE | typeof SYNTHETICS_TLS_RULE | null
->('[UI] TOGGLE ALERT FLYOUT');
+export const setAlertFlyoutVisible = createAction<{
+ id: typeof SYNTHETICS_STATUS_RULE | typeof SYNTHETICS_TLS_RULE | null;
+ isNewRuleFlyout: boolean;
+} | null>('[UI] TOGGLE ALERT FLYOUT');
export const setBasePath = createAction('[UI] SET BASE PATH');
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/index.ts
index 2c7d5e5ce3d4c..f1314bbae4fa0 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/index.ts
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/index.ts
@@ -22,7 +22,8 @@ import {
} from './actions';
export interface UiState {
- alertFlyoutVisible: typeof SYNTHETICS_TLS_RULE | typeof SYNTHETICS_STATUS_RULE | null;
+ ruleFlyoutVisible: typeof SYNTHETICS_TLS_RULE | typeof SYNTHETICS_STATUS_RULE | null;
+ isNewRuleFlyout?: boolean | null;
basePath: string;
esKuery: string;
searchText: string;
@@ -31,7 +32,8 @@ export interface UiState {
}
const initialState: UiState = {
- alertFlyoutVisible: null,
+ isNewRuleFlyout: false,
+ ruleFlyoutVisible: null,
basePath: '',
esKuery: '',
searchText: '',
@@ -45,7 +47,8 @@ export const uiReducer = createReducer(initialState, (builder) => {
state.integrationsPopoverOpen = action.payload;
})
.addCase(setAlertFlyoutVisible, (state, action) => {
- state.alertFlyoutVisible = action.payload;
+ state.ruleFlyoutVisible = action.payload?.id ?? null;
+ state.isNewRuleFlyout = action.payload?.isNewRuleFlyout ?? null;
})
.addCase(setBasePath, (state, action) => {
state.basePath = action.payload;
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/selectors.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/selectors.ts
index f02b1fb564c37..92e5d249a583d 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/selectors.ts
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/selectors.ts
@@ -12,5 +12,10 @@ const uiStateSelector = (appState: SyntheticsAppState) => appState.ui;
export const selectAlertFlyoutVisibility = createSelector(
uiStateSelector,
- ({ alertFlyoutVisible }) => alertFlyoutVisible
+ ({ ruleFlyoutVisible }) => ruleFlyoutVisible
+);
+
+export const selectIsNewRule = createSelector(
+ uiStateSelector,
+ ({ isNewRuleFlyout }) => isNewRuleFlyout
);
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts
index aa1743b7f27db..fe2ad5f7512cc 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts
@@ -24,7 +24,7 @@ import { MonitorDetailsState } from '../../../state';
*/
export const mockState: SyntheticsAppState = {
ui: {
- alertFlyoutVisible: null,
+ ruleFlyoutVisible: null,
basePath: 'yyz',
esKuery: '',
integrationsPopoverOpen: null,
diff --git a/x-pack/plugins/observability_solution/synthetics/scripts/generate_monitors.js b/x-pack/plugins/observability_solution/synthetics/scripts/generate_monitors.js
new file mode 100644
index 0000000000000..c18274f04f5b2
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/scripts/generate_monitors.js
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+require('@kbn/babel-register').install();
+require('./tasks/generate_monitors').generateMonitors();
diff --git a/x-pack/plugins/observability_solution/synthetics/scripts/tasks/generate_monitors.ts b/x-pack/plugins/observability_solution/synthetics/scripts/tasks/generate_monitors.ts
new file mode 100644
index 0000000000000..4e571344ce870
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/scripts/tasks/generate_monitors.ts
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import axios from 'axios';
+import moment from 'moment';
+
+const UP_MONITORS = 0;
+const DOWN_MONITORS = 10;
+
+export const generateMonitors = async () => {
+ // eslint-disable-next-line no-console
+ console.log(`Generating ${UP_MONITORS} up monitors`);
+ for (let i = 0; i < UP_MONITORS; i++) {
+ await createMonitor(getHttpMonitor());
+ }
+
+ // eslint-disable-next-line no-console
+ console.log(`Generating ${DOWN_MONITORS} down monitors`);
+ for (let i = 0; i < DOWN_MONITORS; i++) {
+ await createMonitor(getHttpMonitor(true));
+ }
+};
+
+const createMonitor = async (monitor: any) => {
+ await axios
+ .request({
+ data: monitor,
+ method: 'post',
+ url: 'http://127.0.0.1:5601/test/api/synthetics/monitors',
+ auth: { username: 'elastic', password: 'jdpAyka8HBiq81dFAIB86Nkp' },
+ headers: { 'kbn-xsrf': 'true', 'elastic-api-version': '2023-10-31' },
+ })
+ .catch((error) => {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ });
+};
+
+const getHttpMonitor = (isDown?: boolean) => {
+ return {
+ type: 'http',
+ form_monitor_type: 'http',
+ enabled: true,
+ alert: { status: { enabled: true }, tls: { enabled: true } },
+ schedule: { number: '3', unit: 'm' },
+ 'service.name': '',
+ config_id: '',
+ tags: [],
+ timeout: '16',
+ name: 'Monitor at ' + moment().format('LTS'),
+ locations: [
+ { id: 'us_central_staging', label: 'US Central Staging', isServiceManaged: true },
+ { id: 'us_central', label: 'North America - US Central', isServiceManaged: true },
+ { id: 'us_central_qa', label: 'US Central QA', isServiceManaged: true },
+ ],
+ namespace: 'default',
+ origin: 'ui',
+ journey_id: '',
+ hash: '',
+ id: '',
+ params: '',
+ max_attempts: 2,
+ revision: 1,
+ __ui: { is_tls_enabled: false },
+ urls: 'https://www.google.com',
+ max_redirects: '0',
+ 'url.port': null,
+ password: '',
+ proxy_url: '',
+ proxy_headers: {},
+ 'check.response.body.negative': [],
+ 'check.response.body.positive': isDown ? ["i don't exist"] : [],
+ 'check.response.json': [],
+ 'response.include_body': 'on_error',
+ 'check.response.headers': {},
+ 'response.include_headers': true,
+ 'check.response.status': [],
+ 'check.request.body': { type: 'text', value: '' },
+ 'check.request.headers': {},
+ 'check.request.method': 'GET',
+ username: '',
+ mode: 'any',
+ 'response.include_body_max_bytes': '1024',
+ ipv4: true,
+ ipv6: true,
+ 'ssl.certificate_authorities': '',
+ 'ssl.certificate': '',
+ 'ssl.key': '',
+ 'ssl.key_passphrase': '',
+ 'ssl.verification_mode': 'full',
+ 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
+ };
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/common.test.ts b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/common.test.ts
index 787d35c99b675..34f3be6128a3f 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/common.test.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/common.test.ts
@@ -13,6 +13,9 @@ import {
} from '../../common/runtime_types/alert_rules/common';
const dateFormat = 'MMM D, YYYY @ HH:mm:ss.SSS';
+const monitorName = 'test-monitor';
+const monitorId = '12345';
+const configId = '56789';
describe('updateState', () => {
let spy: jest.SpyInstance;
@@ -190,7 +193,6 @@ describe('updateState', () => {
describe('setRecoveredAlertsContext', () => {
const alertUuid = 'alert-id';
const location = 'us_west';
- const configId = '12345';
const idWithLocation = `${configId}-${location}`;
const basePath = {
publicBaseUrl: 'https://localhost:5601',
@@ -210,10 +212,19 @@ describe('setRecoveredAlertsContext', () => {
},
},
monitor: {
- name: 'test-monitor',
+ name: monitorName,
+ },
+ observer: {
+ geo: {
+ name: location,
+ },
},
} as StaleDownConfig['ping'],
timestamp: new Date().toISOString(),
+ checks: {
+ downWithinXChecks: 1,
+ down: 0,
+ },
},
};
@@ -227,20 +238,23 @@ describe('setRecoveredAlertsContext', () => {
alert: {
getUuid: () => alertUuid,
getId: () => idWithLocation,
- getState: () => ({}),
+ getState: () => ({
+ downThreshold: 1,
+ }),
setContext: jest.fn(),
},
hit: {
'kibana.alert.instance.id': idWithLocation,
'location.id': location,
configId,
+ downThreshold: 1,
},
},
]),
setAlertData: jest.fn(),
isTrackedAlert: jest.fn(),
};
- const staleDownConfigs: Record = {
+ const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {
[idWithLocation]: {
configId,
monitorQueryId: 'stale-config',
@@ -252,11 +266,20 @@ describe('setRecoveredAlertsContext', () => {
id: '123456',
},
monitor: {
- name: 'test-monitor',
+ name: monitorName,
+ },
+ observer: {
+ geo: {
+ name: location,
+ },
},
} as StaleDownConfig['ping'],
timestamp: new Date().toISOString(),
isDeleted: true,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
},
};
setRecoveredAlertsContext({
@@ -267,26 +290,30 @@ describe('setRecoveredAlertsContext', () => {
upConfigs: {},
dateFormat,
tz: 'UTC',
+ groupByLocation: true,
});
expect(alertsClientMock.setAlertData).toBeCalledWith({
id: idWithLocation,
context: {
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
- configId: '12345',
+ configId,
linkMessage: '',
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
- monitorName: 'test-monitor',
- recoveryReason: 'the monitor has been deleted',
- 'kibana.alert.reason': 'the monitor has been deleted',
+ monitorName,
+ recoveryReason: 'has been deleted',
recoveryStatus: 'has been deleted',
monitorUrl: '(unavailable)',
monitorUrlLabel: 'URL',
reason:
- 'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
+ 'Monitor "test-monitor" from us_west is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
stateId: '123456',
status: 'recovered',
locationId: location,
+ locationNames: location,
+ locationName: location,
idWithLocation,
+ timestamp: '2023-02-26T00:00:00.000Z',
+ downThreshold: 1,
},
});
});
@@ -301,7 +328,9 @@ describe('setRecoveredAlertsContext', () => {
alert: {
getUuid: () => alertUuid,
getId: () => idWithLocation,
- getState: () => ({}),
+ getState: () => ({
+ downThreshold: 1,
+ }),
setContext: jest.fn(),
},
hit: {
@@ -314,7 +343,7 @@ describe('setRecoveredAlertsContext', () => {
setAlertData: jest.fn(),
isTrackedAlert: jest.fn(),
};
- const staleDownConfigs: Record = {
+ const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {
[idWithLocation]: {
configId,
monitorQueryId: 'stale-config',
@@ -328,9 +357,18 @@ describe('setRecoveredAlertsContext', () => {
monitor: {
name: 'test-monitor',
},
+ observer: {
+ geo: {
+ name: 'us_west',
+ },
+ },
} as StaleDownConfig['ping'],
timestamp: new Date().toISOString(),
isLocationRemoved: true,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
},
};
setRecoveredAlertsContext({
@@ -341,26 +379,30 @@ describe('setRecoveredAlertsContext', () => {
upConfigs: {},
dateFormat,
tz: 'UTC',
+ groupByLocation: true,
});
expect(alertsClientMock.setAlertData).toBeCalledWith({
id: idWithLocation,
context: {
- configId: '12345',
+ configId,
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
monitorUrl: '(unavailable)',
- reason:
- 'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
+ idWithLocation,
linkMessage: '',
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
- monitorName: 'test-monitor',
+ monitorName,
recoveryReason: 'this location has been removed from the monitor',
- 'kibana.alert.reason': 'this location has been removed from the monitor',
recoveryStatus: 'has recovered',
stateId: '123456',
status: 'recovered',
monitorUrlLabel: 'URL',
- idWithLocation,
+ timestamp: '2023-02-26T00:00:00.000Z',
+ locationName: location,
+ locationNames: location,
+ reason:
+ 'Monitor "test-monitor" from us_west is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
locationId: location,
+ downThreshold: 1,
},
});
});
@@ -375,7 +417,9 @@ describe('setRecoveredAlertsContext', () => {
alert: {
getId: () => idWithLocation,
getUuid: () => alertUuid,
- getState: () => ({}),
+ getState: () => ({
+ downThreshold: 1,
+ }),
setContext: jest.fn(),
},
hit: {
@@ -388,7 +432,7 @@ describe('setRecoveredAlertsContext', () => {
setAlertData: jest.fn(),
isTrackedAlert: jest.fn(),
};
- const staleDownConfigs: Record = {
+ const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {
[idWithLocation]: {
configId,
monitorQueryId: 'stale-config',
@@ -405,6 +449,10 @@ describe('setRecoveredAlertsContext', () => {
} as StaleDownConfig['ping'],
timestamp: new Date().toISOString(),
isLocationRemoved: true,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
},
};
setRecoveredAlertsContext({
@@ -415,6 +463,7 @@ describe('setRecoveredAlertsContext', () => {
upConfigs,
dateFormat,
tz: 'UTC',
+ groupByLocation: true,
});
expect(alertsClientMock.setAlertData).toBeCalledWith({
id: idWithLocation,
@@ -422,22 +471,250 @@ describe('setRecoveredAlertsContext', () => {
configId,
idWithLocation,
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
- monitorName: 'test-monitor',
+ monitorName,
status: 'up',
recoveryReason:
'the monitor is now up again. It ran successfully at Feb 26, 2023 @ 00:00:00.000',
- 'kibana.alert.reason':
- 'the monitor is now up again. It ran successfully at Feb 26, 2023 @ 00:00:00.000',
recoveryStatus: 'is now up',
locationId: location,
+ locationNames: location,
+ locationName: location,
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
- linkMessage:
- '- Link: https://localhost:5601/app/synthetics/monitor/12345/errors/123456?locationId=us_west',
+ linkMessage: `- Link: https://localhost:5601/app/synthetics/monitor/${configId}/errors/123456?locationId=us_west`,
+ monitorUrl: '(unavailable)',
+ monitorUrlLabel: 'URL',
+ reason:
+ 'Monitor "test-monitor" from us_west is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
+ timestamp: '2023-02-26T00:00:00.000Z',
+ downThreshold: 1,
+ stateId: '123456',
+ },
+ });
+ });
+
+ it('sets the correct default recovery summary', () => {
+ const alertsClientMock = {
+ report: jest.fn(),
+ getAlertLimitValue: jest.fn().mockReturnValue(10),
+ setAlertLimitReached: jest.fn(),
+ getRecoveredAlerts: jest.fn().mockReturnValue([
+ {
+ alert: {
+ getId: () => idWithLocation,
+ getUuid: () => alertUuid,
+ getState: () => ({
+ downThreshold: 1,
+ }),
+ setContext: jest.fn(),
+ },
+ hit: {
+ 'kibana.alert.instance.id': idWithLocation,
+ 'location.id': location,
+ 'monitor.name': monitorName,
+ 'monitor.id': monitorId,
+ '@timestamp': new Date().toISOString(),
+ 'agent.name': 'test-host',
+ 'observer.geo.name': 'Unnamed-location',
+ 'observer.name.keyword': 'Unnamed-location-id',
+ 'monitor.type': 'HTTP',
+ 'error.message': 'test-error-message',
+ configId,
+ },
+ },
+ ]),
+ setAlertData: jest.fn(),
+ isTrackedAlert: jest.fn(),
+ };
+ const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {};
+ setRecoveredAlertsContext({
+ alertsClient: alertsClientMock,
+ basePath,
+ spaceId: 'default',
+ staleDownConfigs,
+ upConfigs: {},
+ dateFormat,
+ tz: 'UTC',
+ groupByLocation: true,
+ });
+ expect(alertsClientMock.setAlertData).toBeCalledWith({
+ id: idWithLocation,
+ context: {
+ configId,
+ idWithLocation,
+ alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
+ monitorName,
+ monitorId,
+ status: 'recovered',
+ recoveryReason: 'the alert condition is no longer met',
+ recoveryStatus: 'has recovered',
+ locationId: location,
+ checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
+ linkMessage: '',
monitorUrl: '(unavailable)',
monitorUrlLabel: 'URL',
reason:
- 'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
- stateId: null,
+ 'Monitor "test-monitor" from Unnamed-location is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
+ timestamp: '2023-02-26T00:00:00.000Z',
+ downThreshold: 1,
+ locationNames: 'Unnamed-location',
+ locationName: 'Unnamed-location',
+ lastErrorMessage: 'test-error-message',
+ monitorType: 'HTTP',
+ hostName: 'test-host',
+ },
+ });
+ });
+
+ it('sets the recovery summary for recovered custom alerts', () => {
+ const alertsClientMock = {
+ report: jest.fn(),
+ getAlertLimitValue: jest.fn().mockReturnValue(10),
+ setAlertLimitReached: jest.fn(),
+ getRecoveredAlerts: jest.fn().mockReturnValue([
+ {
+ alert: {
+ getId: () => idWithLocation,
+ getUuid: () => alertUuid,
+ getState: () => ({
+ downThreshold: 1,
+ configId,
+ }),
+ setContext: jest.fn(),
+ },
+ hit: {
+ 'kibana.alert.instance.id': idWithLocation,
+ 'location.id': ['us_central', 'us_west'],
+ 'monitor.name': monitorName,
+ 'monitor.id': monitorId,
+ 'monitor.type': 'HTTP',
+ 'monitor.state.id': '123456',
+ '@timestamp': new Date().toISOString(),
+ 'observer.geo.name': ['us-central', 'us-east'],
+ 'error.message': 'test-error-message',
+ 'url.full': 'http://test_url.com',
+ configId,
+ 'agent.name': 'test-agent',
+ },
+ },
+ ]),
+ setAlertData: jest.fn(),
+ isTrackedAlert: jest.fn(),
+ };
+ const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {};
+ setRecoveredAlertsContext({
+ alertsClient: alertsClientMock,
+ basePath,
+ spaceId: 'default',
+ staleDownConfigs,
+ upConfigs: {},
+ dateFormat,
+ tz: 'UTC',
+ groupByLocation: true,
+ });
+ expect(alertsClientMock.setAlertData).toBeCalledWith({
+ id: idWithLocation,
+ context: {
+ configId,
+ idWithLocation,
+ alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
+ monitorName,
+ monitorId,
+ status: 'recovered',
+ recoveryReason: 'the alert condition is no longer met',
+ recoveryStatus: 'has recovered',
+ locationId: 'us_central and us_west',
+ checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
+ linkMessage:
+ '- Link: https://localhost:5601/app/synthetics/monitor/56789/errors/123456?locationId=us_central',
+ monitorUrl: 'http://test_url.com',
+ hostName: 'test-agent',
+ monitorUrlLabel: 'URL',
+ reason:
+ 'Monitor "test-monitor" from us-central and us-east is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
+ stateId: '123456',
+ timestamp: '2023-02-26T00:00:00.000Z',
+ downThreshold: 1,
+ locationNames: 'us-central and us-east',
+ locationName: 'us-central and us-east',
+ monitorType: 'HTTP',
+ lastErrorMessage: 'test-error-message',
+ },
+ });
+ });
+
+ it('handles ungrouped recoveries', () => {
+ const alertsClientMock = {
+ report: jest.fn(),
+ getAlertLimitValue: jest.fn().mockReturnValue(10),
+ setAlertLimitReached: jest.fn(),
+ getRecoveredAlerts: jest.fn().mockReturnValue([
+ {
+ alert: {
+ getId: () => idWithLocation,
+ getUuid: () => alertUuid,
+ getState: () => ({
+ downThreshold: 1,
+ configId,
+ }),
+ setContext: jest.fn(),
+ },
+ hit: {
+ 'kibana.alert.instance.id': idWithLocation,
+ 'location.id': location,
+ 'monitor.name': monitorName,
+ 'monitor.type': 'HTTP',
+ 'monitor.id': monitorId,
+ 'agent.name': 'test-agent',
+ '@timestamp': new Date().toISOString(),
+ 'observer.geo.name': ['us-central', 'us-east'],
+ 'error.message': 'test-error-message',
+ 'url.full': 'http://test_url.com',
+ 'monitor.state.id': '123456',
+ configId,
+ },
+ },
+ ]),
+ setAlertData: jest.fn(),
+ isTrackedAlert: jest.fn(),
+ };
+ const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {};
+ setRecoveredAlertsContext({
+ alertsClient: alertsClientMock,
+ basePath,
+ spaceId: 'default',
+ staleDownConfigs,
+ upConfigs: {},
+ dateFormat,
+ tz: 'UTC',
+ groupByLocation: false,
+ });
+ expect(alertsClientMock.setAlertData).toBeCalledWith({
+ id: idWithLocation,
+ context: {
+ configId,
+ idWithLocation,
+ alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
+ monitorName,
+ monitorId,
+ status: 'recovered',
+ recoveryReason: 'the alert condition is no longer met',
+ recoveryStatus: 'has recovered',
+ locationId: location,
+ checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
+ linkMessage:
+ '- Link: https://localhost:5601/app/synthetics/monitor/56789/errors/123456?locationId=us_west',
+ monitorUrl: 'http://test_url.com',
+ hostName: 'test-agent',
+ monitorUrlLabel: 'URL',
+ reason:
+ 'Monitor "test-monitor" from us-central and us-east is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
+ stateId: '123456',
+ timestamp: '2023-02-26T00:00:00.000Z',
+ downThreshold: 1,
+ locationNames: 'us-central and us-east',
+ locationName: 'us-central and us-east',
+ monitorType: 'HTTP',
+ lastErrorMessage: 'test-error-message',
},
});
});
diff --git a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/common.ts b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/common.ts
index 18ade57662ed3..c1bf18e18e90b 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/common.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/common.ts
@@ -4,24 +4,26 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
-import moment, { Moment } from 'moment';
+import moment from 'moment';
import { isRight } from 'fp-ts/lib/Either';
import Mustache from 'mustache';
import { IBasePath } from '@kbn/core/server';
import {
- IRuleTypeAlerts,
ActionGroupIdsOf,
AlertInstanceContext as AlertContext,
AlertInstanceState as AlertState,
+ IRuleTypeAlerts,
} from '@kbn/alerting-plugin/server';
import { getAlertDetailsUrl } from '@kbn/observability-plugin/common';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
import { i18n } from '@kbn/i18n';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
-import { legacyExperimentalFieldMap, ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils';
-import { PublicAlertsClient } from '@kbn/alerting-plugin/server/alerts_client/types';
-import { ALERT_REASON } from '@kbn/rule-data-utils';
+import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils';
+import {
+ PublicAlertsClient,
+ RecoveredAlertData,
+} from '@kbn/alerting-plugin/server/alerts_client/types';
+import { StatusRuleParams, TimeWindow } from '../../common/rules/status_rule';
import { syntheticsRuleFieldMap } from '../../common/rules/synthetics_rule_field_map';
import { combineFiltersAndUserSearch, stringifyKueries } from '../../common/lib';
import {
@@ -29,17 +31,18 @@ import {
SYNTHETICS_RULE_TYPES_ALERT_CONTEXT,
} from '../../common/constants/synthetics_alerts';
import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../queries/get_index_pattern';
-import { StatusCheckFilters } from '../../common/runtime_types';
+import { OverviewPing, StatusCheckFilters } from '../../common/runtime_types';
import { SyntheticsEsClient } from '../lib';
import { getMonitorSummary } from './status_rule/message_utils';
import {
AlertOverviewStatus,
SyntheticsCommonState,
SyntheticsCommonStateCodec,
+ SyntheticsMonitorStatusAlertState,
} from '../../common/runtime_types/alert_rules/common';
import { getSyntheticsErrorRouteFromMonitorId } from '../../common/utils/get_synthetics_monitor_url';
import { ALERT_DETAILS_URL, RECOVERY_REASON } from './action_variables';
-import type { MonitorSummaryStatusRule } from './status_rule/types';
+import type { MonitorStatusAlertDocument, MonitorSummaryStatusRule } from './status_rule/types';
export const updateState = (
state: SyntheticsCommonState,
@@ -124,70 +127,58 @@ export const getRelativeViewInAppUrl = ({
stateId: string;
locationId: string;
}) => {
- const relativeViewInAppUrl = getSyntheticsErrorRouteFromMonitorId({
+ return getSyntheticsErrorRouteFromMonitorId({
configId,
stateId,
locationId,
});
-
- return relativeViewInAppUrl;
-};
-
-export const getErrorDuration = (startedAt: Moment, endsAt: Moment) => {
- const diffInDays = endsAt.diff(startedAt, 'days');
- if (diffInDays > 1) {
- return i18n.translate('xpack.synthetics.errorDetails.errorDuration.days', {
- defaultMessage: '{value} days',
- values: { value: diffInDays },
- });
- }
- const diffInHours = endsAt.diff(startedAt, 'hours');
- if (diffInHours > 1) {
- return i18n.translate('xpack.synthetics.errorDetails.errorDuration.hours', {
- defaultMessage: '{value} hours',
- values: { value: diffInHours },
- });
- }
- const diffInMinutes = endsAt.diff(startedAt, 'minutes');
- return i18n.translate('xpack.synthetics.errorDetails.errorDuration.mins', {
- defaultMessage: '{value} mins',
- values: { value: diffInMinutes },
- });
};
export const setRecoveredAlertsContext = ({
alertsClient,
basePath,
spaceId,
- staleDownConfigs,
+ staleDownConfigs = {},
upConfigs,
dateFormat,
tz,
+ params,
+ groupByLocation,
}: {
alertsClient: PublicAlertsClient<
- ObservabilityUptimeAlert,
- AlertState,
+ MonitorStatusAlertDocument,
+ SyntheticsMonitorStatusAlertState,
AlertContext,
ActionGroupIdsOf
>;
basePath?: IBasePath;
spaceId?: string;
+ params?: StatusRuleParams;
staleDownConfigs: AlertOverviewStatus['staleDownConfigs'];
upConfigs: AlertOverviewStatus['upConfigs'];
dateFormat: string;
tz: string;
+ groupByLocation: boolean;
}) => {
const recoveredAlerts = alertsClient.getRecoveredAlerts() ?? [];
for (const recoveredAlert of recoveredAlerts) {
const recoveredAlertId = recoveredAlert.alert.getId();
const alertUuid = recoveredAlert.alert.getUuid();
-
- const state = recoveredAlert.alert.getState();
const alertHit = recoveredAlert.hit;
- const locationId = alertHit?.['location.id'];
+ const alertState = recoveredAlert.alert.getState();
const configId = alertHit?.configId;
+ const locationIds = alertHit?.['location.id'] ? [alertHit?.['location.id']].flat() : [];
+ const locationName = alertHit?.['observer.geo.name']
+ ? [alertHit?.['observer.geo.name']].flat()
+ : [];
+ let syntheticsStateId = alertHit?.['monitor.state.id'];
- let recoveryReason = '';
+ let recoveryReason = i18n.translate(
+ 'xpack.synthetics.alerts.monitorStatus.defaultRecovery.reason',
+ {
+ defaultMessage: `the alert condition is no longer met`,
+ }
+ );
let recoveryStatus = i18n.translate(
'xpack.synthetics.alerts.monitorStatus.defaultRecovery.status',
{
@@ -195,107 +186,94 @@ export const setRecoveredAlertsContext = ({
}
);
let isUp = false;
- let linkMessage = '';
- let monitorSummary: MonitorSummaryStatusRule | null = null;
- let lastErrorMessage;
-
- if (recoveredAlertId && locationId && staleDownConfigs[recoveredAlertId]) {
- const downConfig = staleDownConfigs[recoveredAlertId];
- const { ping } = downConfig;
- monitorSummary = getMonitorSummary(
- ping,
- RECOVERED_LABEL,
- locationId,
- downConfig.configId,
+ let linkMessage = getDefaultLinkMessage({
+ basePath,
+ spaceId,
+ syntheticsStateId,
+ configId,
+ locationId: locationIds[0],
+ });
+ let monitorSummary: MonitorSummaryStatusRule | undefined = getDefaultRecoveredSummary({
+ recoveredAlert,
+ tz,
+ dateFormat,
+ params,
+ });
+ let lastErrorMessage = alertHit?.['error.message'];
+
+ if (!groupByLocation && monitorSummary) {
+ const formattedLocationNames = locationName.join(` ${AND_LABEL} `);
+ const formattedLocationIds = locationIds.join(` ${AND_LABEL} `);
+ monitorSummary.locationNames = formattedLocationNames;
+ monitorSummary.locationName = formattedLocationNames;
+ monitorSummary.locationId = formattedLocationIds;
+ }
+
+ if (recoveredAlertId && locationIds && staleDownConfigs[recoveredAlertId]) {
+ const summary = getDeletedMonitorOrLocationSummary({
+ staleDownConfigs,
+ recoveredAlertId,
+ locationIds,
dateFormat,
- tz
- );
- lastErrorMessage = monitorSummary.lastErrorMessage;
-
- if (downConfig.isDeleted) {
- recoveryStatus = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.deleteMonitor.status',
- {
- defaultMessage: `has been deleted`,
- }
- );
- recoveryReason = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.deleteMonitor.reason',
- {
- defaultMessage: `the monitor has been deleted`,
- }
- );
- } else if (downConfig.isLocationRemoved) {
- recoveryStatus = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.removedLocation.status',
- {
- defaultMessage: `has recovered`,
- }
- );
- recoveryReason = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.removedLocation.reason',
- {
- defaultMessage: `this location has been removed from the monitor`,
- }
- );
+ tz,
+ params,
+ });
+ if (summary) {
+ monitorSummary = {
+ ...monitorSummary,
+ ...summary.monitorSummary,
+ };
+ recoveryStatus = summary.recoveryStatus;
+ recoveryReason = summary.recoveryReason;
+ lastErrorMessage = summary.lastErrorMessage;
+ syntheticsStateId = summary.stateId ? summary.stateId : syntheticsStateId;
}
+ // Cannot display link message for deleted monitors or deleted locations
+ linkMessage = '';
}
- if (configId && recoveredAlertId && locationId && upConfigs[recoveredAlertId]) {
- // pull the last error from state, since it is not available on the up ping
- lastErrorMessage = alertHit?.['error.message'];
-
- const upConfig = upConfigs[recoveredAlertId];
- isUp = Boolean(upConfig) || false;
- const ping = upConfig.ping;
-
- monitorSummary = getMonitorSummary(
- ping,
- RECOVERED_LABEL,
- locationId,
+ if (configId && recoveredAlertId && locationIds && upConfigs[recoveredAlertId]) {
+ const summary = getUpMonitorRecoverySummary({
+ upConfigs,
+ recoveredAlertId,
+ alertHit,
+ locationIds,
configId,
+ basePath,
+ spaceId,
dateFormat,
- tz
- );
-
- // When alert is flapping, the stateId is not available on ping.state.ends.id, use state instead
- const stateId = ping.state?.ends?.id || state.stateId;
- const upTimestamp = ping['@timestamp'];
- const checkedAt = moment(upTimestamp).tz(tz).format(dateFormat);
- recoveryStatus = i18n.translate('xpack.synthetics.alerts.monitorStatus.upCheck.status', {
- defaultMessage: `is now up`,
+ tz,
+ params,
});
- recoveryReason = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.upCheck.reasonWithoutDuration',
- {
- defaultMessage: `the monitor is now up again. It ran successfully at {checkedAt}`,
- values: {
- checkedAt,
- },
- }
- );
-
- if (basePath && spaceId && stateId) {
- const relativeViewInAppUrl = getRelativeViewInAppUrl({
- configId,
- locationId,
- stateId,
- });
- linkMessage = getFullViewInAppMessage(basePath, spaceId, relativeViewInAppUrl);
+ if (summary) {
+ monitorSummary = {
+ ...monitorSummary,
+ ...summary.monitorSummary,
+ };
+ recoveryStatus = summary.recoveryStatus;
+ recoveryReason = summary.recoveryReason;
+ isUp = summary.isUp;
+ lastErrorMessage = summary.lastErrorMessage;
+ linkMessage = summary.linkMessage ? summary.linkMessage : linkMessage;
+ syntheticsStateId = summary.stateId ? summary.stateId : syntheticsStateId;
}
}
const context = {
- ...state,
+ ...alertState,
...(monitorSummary ? monitorSummary : {}),
- locationId,
+ locationId: locationIds.join(` ${AND_LABEL} `),
idWithLocation: recoveredAlertId,
lastErrorMessage,
recoveryStatus,
linkMessage,
+ stateId: syntheticsStateId,
...(isUp ? { status: 'up' } : {}),
- ...(recoveryReason ? { [RECOVERY_REASON]: recoveryReason } : {}),
- ...(recoveryReason ? { [ALERT_REASON]: recoveryReason } : {}),
+ ...(recoveryReason
+ ? {
+ [RECOVERY_REASON]: recoveryReason,
+ }
+ : {}),
...(basePath && spaceId && alertUuid
? { [ALERT_DETAILS_URL]: getAlertDetailsUrl(basePath, spaceId, alertUuid) }
: {}),
@@ -304,6 +282,220 @@ export const setRecoveredAlertsContext = ({
}
};
+export const getDefaultLinkMessage = ({
+ basePath,
+ spaceId,
+ syntheticsStateId,
+ configId,
+ locationId,
+}: {
+ basePath?: IBasePath;
+ spaceId?: string;
+ syntheticsStateId?: string;
+ configId?: string;
+ locationId?: string;
+}) => {
+ if (basePath && spaceId && syntheticsStateId && configId && locationId) {
+ const relativeViewInAppUrl = getRelativeViewInAppUrl({
+ configId,
+ locationId,
+ stateId: syntheticsStateId,
+ });
+ return getFullViewInAppMessage(basePath, spaceId, relativeViewInAppUrl);
+ } else {
+ return '';
+ }
+};
+
+export const getDefaultRecoveredSummary = ({
+ recoveredAlert,
+ tz,
+ dateFormat,
+ params,
+}: {
+ recoveredAlert: RecoveredAlertData<
+ MonitorStatusAlertDocument,
+ AlertState,
+ AlertContext,
+ ActionGroupIdsOf
+ >;
+ tz: string;
+ dateFormat: string;
+ params?: StatusRuleParams;
+}) => {
+ if (!recoveredAlert.hit) return; // TODO: handle this case
+ const hit = recoveredAlert.hit;
+ const locationId = hit['location.id'];
+ const configId = hit.configId;
+ return getMonitorSummary({
+ monitorInfo: {
+ monitor: {
+ id: hit['monitor.id'],
+ name: hit['monitor.name'],
+ type: hit['monitor.type'],
+ },
+ config_id: configId,
+ observer: {
+ geo: {
+ name: hit['observer.geo.name'] || hit['location.name'],
+ },
+ name: locationId,
+ },
+ agent: {
+ name: hit['agent.name'] || '',
+ },
+ '@timestamp': String(hit['@timestamp']),
+ ...(hit['error.message'] ? { error: { message: hit['error.message'] } } : {}),
+ ...(hit['url.full'] ? { url: { full: hit['url.full'] } } : {}),
+ } as unknown as OverviewPing,
+ statusMessage: RECOVERED_LABEL,
+ locationId,
+ configId,
+ dateFormat,
+ tz,
+ params,
+ });
+};
+
+export const getDeletedMonitorOrLocationSummary = ({
+ staleDownConfigs,
+ recoveredAlertId,
+ locationIds,
+ dateFormat,
+ tz,
+ params,
+}: {
+ staleDownConfigs: AlertOverviewStatus['staleDownConfigs'];
+ recoveredAlertId: string;
+ locationIds: string[];
+ dateFormat: string;
+ tz: string;
+ params?: StatusRuleParams;
+}) => {
+ const downConfig = staleDownConfigs[recoveredAlertId];
+ const { ping } = downConfig;
+ const monitorSummary = getMonitorSummary({
+ monitorInfo: ping,
+ statusMessage: RECOVERED_LABEL,
+ locationId: locationIds,
+ configId: downConfig.configId,
+ dateFormat,
+ tz,
+ params,
+ });
+ const lastErrorMessage = monitorSummary.lastErrorMessage;
+
+ if (downConfig.isDeleted) {
+ return {
+ lastErrorMessage,
+ monitorSummary,
+ stateId: ping.state?.id,
+ recoveryStatus: i18n.translate('xpack.synthetics.alerts.monitorStatus.deleteMonitor.status', {
+ defaultMessage: `has been deleted`,
+ }),
+ recoveryReason: i18n.translate('xpack.synthetics.alerts.monitorStatus.deleteMonitor.status', {
+ defaultMessage: `has been deleted`,
+ }),
+ };
+ } else if (downConfig.isLocationRemoved) {
+ return {
+ monitorSummary,
+ lastErrorMessage,
+ stateId: ping.state?.id,
+ recoveryStatus: i18n.translate(
+ 'xpack.synthetics.alerts.monitorStatus.removedLocation.status',
+ {
+ defaultMessage: `has recovered`,
+ }
+ ),
+ recoveryReason: i18n.translate(
+ 'xpack.synthetics.alerts.monitorStatus.removedLocation.reason',
+ {
+ defaultMessage: `this location has been removed from the monitor`,
+ }
+ ),
+ };
+ }
+};
+
+export const getUpMonitorRecoverySummary = ({
+ upConfigs,
+ recoveredAlertId,
+ alertHit,
+ locationIds,
+ configId,
+ basePath,
+ spaceId,
+ dateFormat,
+ tz,
+ params,
+}: {
+ upConfigs: AlertOverviewStatus['upConfigs'];
+ recoveredAlertId: string;
+ alertHit: any;
+ locationIds: string[];
+ configId: string;
+ basePath?: IBasePath;
+ spaceId?: string;
+ dateFormat: string;
+ tz: string;
+ params?: StatusRuleParams;
+}) => {
+ // pull the last error from state, since it is not available on the up ping
+ const lastErrorMessage = alertHit?.['error.message'];
+ let linkMessage = '';
+
+ const upConfig = upConfigs[recoveredAlertId];
+ const isUp = Boolean(upConfig) || false;
+ const ping = upConfig.ping;
+
+ const monitorSummary = getMonitorSummary({
+ monitorInfo: ping,
+ statusMessage: RECOVERED_LABEL,
+ locationId: locationIds,
+ configId,
+ dateFormat,
+ tz,
+ params,
+ });
+
+ // When alert is flapping, the stateId is not available on ping.state.ends.id, use state instead
+ const stateId = ping.state?.ends?.id;
+ const upTimestamp = ping['@timestamp'];
+ const checkedAt = moment(upTimestamp).tz(tz).format(dateFormat);
+ const recoveryStatus = i18n.translate('xpack.synthetics.alerts.monitorStatus.upCheck.status', {
+ defaultMessage: `is now up`,
+ });
+ const recoveryReason = i18n.translate(
+ 'xpack.synthetics.alerts.monitorStatus.upCheck.reasonWithoutDuration',
+ {
+ defaultMessage: `the monitor is now up again. It ran successfully at {checkedAt}`,
+ values: {
+ checkedAt,
+ },
+ }
+ );
+
+ if (basePath && spaceId && stateId) {
+ const relativeViewInAppUrl = getRelativeViewInAppUrl({
+ configId,
+ locationId: locationIds[0],
+ stateId,
+ });
+ linkMessage = getFullViewInAppMessage(basePath, spaceId, relativeViewInAppUrl);
+ }
+
+ return {
+ monitorSummary,
+ lastErrorMessage,
+ recoveryStatus,
+ recoveryReason,
+ isUp,
+ linkMessage,
+ stateId,
+ };
+};
+
export const RECOVERED_LABEL = i18n.translate('xpack.synthetics.monitorStatus.recoveredLabel', {
defaultMessage: 'recovered',
});
@@ -355,9 +547,39 @@ export const syntheticsRuleTypeFieldMap = {
...legacyExperimentalFieldMap,
};
-export const SyntheticsRuleTypeAlertDefinition: IRuleTypeAlerts = {
+export const SyntheticsRuleTypeAlertDefinition: IRuleTypeAlerts = {
context: SYNTHETICS_RULE_TYPES_ALERT_CONTEXT,
mappings: { fieldMap: syntheticsRuleTypeFieldMap },
useLegacyAlerts: true,
shouldWrite: true,
};
+
+export function getTimeUnitLabel(timeWindow: TimeWindow) {
+ const { size: timeValue = 1, unit: timeUnit } = timeWindow;
+ switch (timeUnit) {
+ case 's':
+ return i18n.translate('xpack.synthetics.timeUnits.secondLabel', {
+ defaultMessage: '{timeValue, plural, one {second} other {seconds}}',
+ values: { timeValue },
+ });
+ case 'm':
+ return i18n.translate('xpack.synthetics.timeUnits.minuteLabel', {
+ defaultMessage: '{timeValue, plural, one {minute} other {minutes}}',
+ values: { timeValue },
+ });
+ case 'h':
+ return i18n.translate('xpack.synthetics.timeUnits.hourLabel', {
+ defaultMessage: '{timeValue, plural, one {hour} other {hours}}',
+ values: { timeValue },
+ });
+ case 'd':
+ return i18n.translate('xpack.synthetics.timeUnits.dayLabel', {
+ defaultMessage: '{timeValue, plural, one {day} other {days}}',
+ values: { timeValue },
+ });
+ }
+}
+
+export const AND_LABEL = i18n.translate('xpack.synthetics.alerts.monitorStatus.andLabel', {
+ defaultMessage: 'and',
+});
diff --git a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/message_utils.ts b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/message_utils.ts
index 22b15b5cdefd0..a0a14ddaebfed 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/message_utils.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/message_utils.ts
@@ -8,6 +8,9 @@
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { ALERT_REASON } from '@kbn/rule-data-utils';
+import { AlertStatusMetaData } from '../../../common/runtime_types/alert_rules/common';
+import { getConditionType, StatusRuleParams } from '../../../common/rules/status_rule';
+import { AND_LABEL, getTimeUnitLabel } from '../common';
import { ALERT_REASON_MSG } from '../action_variables';
import { MonitorSummaryStatusRule } from './types';
import {
@@ -15,6 +18,7 @@ import {
MONITOR_TYPE,
MONITOR_NAME,
OBSERVER_GEO_NAME,
+ OBSERVER_NAME,
URL_FULL,
ERROR_MESSAGE,
AGENT_NAME,
@@ -23,33 +27,64 @@ import {
import { OverviewPing } from '../../../common/runtime_types';
import { UNNAMED_LOCATION } from '../../../common/constants';
-export const getMonitorAlertDocument = (monitorSummary: MonitorSummaryStatusRule) => ({
+export const getMonitorAlertDocument = (
+ monitorSummary: MonitorSummaryStatusRule,
+ locationNames: string[],
+ locationIds: string[],
+ useLatestChecks: boolean
+) => ({
[MONITOR_ID]: monitorSummary.monitorId,
[MONITOR_TYPE]: monitorSummary.monitorType,
[MONITOR_NAME]: monitorSummary.monitorName,
[URL_FULL]: monitorSummary.monitorUrl,
- [OBSERVER_GEO_NAME]: monitorSummary.locationName,
+ [OBSERVER_GEO_NAME]: locationNames,
+ [OBSERVER_NAME]: locationIds,
[ERROR_MESSAGE]: monitorSummary.lastErrorMessage,
[AGENT_NAME]: monitorSummary.hostName,
[ALERT_REASON]: monitorSummary.reason,
[STATE_ID]: monitorSummary.stateId,
- 'location.id': monitorSummary.locationId,
- 'location.name': monitorSummary.locationName,
+ 'location.id': locationIds,
+ 'location.name': locationNames,
configId: monitorSummary.configId,
+ 'kibana.alert.evaluation.threshold': monitorSummary.downThreshold,
+ 'kibana.alert.evaluation.value':
+ (useLatestChecks ? monitorSummary.checks?.downWithinXChecks : monitorSummary.checks?.down) ?? 1,
'monitor.tags': monitorSummary.monitorTags ?? [],
});
-export const getMonitorSummary = (
- monitorInfo: OverviewPing,
- statusMessage: string,
- locationId: string,
- configId: string,
- dateFormat: string,
- tz: string
-): MonitorSummaryStatusRule => {
- const monitorName = monitorInfo.monitor?.name ?? monitorInfo.monitor?.id;
- const observerLocation = monitorInfo.observer?.geo?.name ?? UNNAMED_LOCATION;
- const checkedAt = moment(monitorInfo['@timestamp']).tz(tz).format(dateFormat);
+export interface MonitorSummaryData {
+ monitorInfo: OverviewPing;
+ statusMessage: string;
+ locationId: string[];
+ configId: string;
+ dateFormat: string;
+ tz: string;
+ checks?: {
+ downWithinXChecks: number;
+ down: number;
+ };
+ params?: StatusRuleParams;
+}
+
+export const getMonitorSummary = ({
+ monitorInfo,
+ locationId,
+ configId,
+ tz,
+ dateFormat,
+ statusMessage,
+ checks,
+ params,
+}: MonitorSummaryData): MonitorSummaryStatusRule => {
+ const { downThreshold } = getConditionType(params?.condition);
+ const monitorName = monitorInfo?.monitor?.name ?? monitorInfo?.monitor?.id;
+ const locationName = monitorInfo?.observer?.geo?.name ?? UNNAMED_LOCATION;
+ const formattedLocationName = Array.isArray(locationName)
+ ? locationName.join(` ${AND_LABEL} `)
+ : locationName;
+ const checkedAt = moment(monitorInfo?.['@timestamp'])
+ .tz(tz || 'UTC')
+ .format(dateFormat);
const typeToLabelMap: Record = {
http: 'HTTP',
tcp: 'TCP',
@@ -65,11 +100,11 @@ export const getMonitorSummary = (
browser: 'URL',
};
const monitorType = monitorInfo.monitor?.type;
- const stateId = monitorInfo.state?.id || null;
+ const stateId = monitorInfo.state?.id;
return {
checkedAt,
- locationId,
+ locationId: locationId?.join?.(` ${AND_LABEL} `) ?? '',
configId,
monitorUrl: monitorInfo.url?.full || UNAVAILABLE_LABEL,
monitorUrlLabel: typeToUrlLabelMap[monitorType] || 'URL',
@@ -77,40 +112,162 @@ export const getMonitorSummary = (
monitorName,
monitorType: typeToLabelMap[monitorInfo.monitor?.type] || monitorInfo.monitor?.type,
lastErrorMessage: monitorInfo.error?.message!,
- locationName: monitorInfo.observer?.geo?.name!,
+ locationName: formattedLocationName,
+ locationNames: formattedLocationName,
hostName: monitorInfo.agent?.name!,
status: statusMessage,
stateId,
[ALERT_REASON_MSG]: getReasonMessage({
name: monitorName,
- location: observerLocation,
+ location: formattedLocationName,
status: statusMessage,
- timestamp: monitorInfo['@timestamp'],
+ checks,
+ params,
}),
+ checks,
+ downThreshold,
+ timestamp: monitorInfo['@timestamp'],
monitorTags: monitorInfo.tags,
};
};
+export const getUngroupedReasonMessage = ({
+ statusConfigs,
+ monitorName,
+ params,
+ status = DOWN_LABEL,
+}: {
+ statusConfigs: AlertStatusMetaData[];
+ monitorName: string;
+ params: StatusRuleParams;
+ status?: string;
+ checks?: {
+ downWithinXChecks: number;
+ down: number;
+ };
+}) => {
+ const { useLatestChecks, numberOfChecks, timeWindow, downThreshold, locationsThreshold } =
+ getConditionType(params.condition);
+
+ return i18n.translate(
+ 'xpack.synthetics.alertRules.monitorStatus.reasonMessage.location.ungrouped.multiple',
+ {
+ defaultMessage: `Monitor "{name}" is {status} {locationDetails}. Alert when down {threshold} {threshold, plural, one {time} other {times}} {condition} from at least {locationsThreshold} {locationsThreshold, plural, one {location} other {locations}}.`,
+ values: {
+ name: monitorName,
+ status,
+ threshold: downThreshold,
+ locationsThreshold,
+ condition: useLatestChecks
+ ? i18n.translate(
+ 'xpack.synthetics.alertRules.monitorStatus.reasonMessage.condition.latestChecks',
+ {
+ defaultMessage: 'out of the last {numberOfChecks} checks',
+ values: { numberOfChecks },
+ }
+ )
+ : i18n.translate(
+ 'xpack.synthetics.alertRules.monitorStatus.reasonMessage.condition.timeWindow',
+ {
+ defaultMessage: 'within the last {time} {unit}',
+ values: {
+ time: timeWindow.size,
+ unit: getTimeUnitLabel(timeWindow),
+ },
+ }
+ ),
+ locationDetails: statusConfigs
+ .map((c) => {
+ return i18n.translate(
+ 'xpack.synthetics.alertRules.monitorStatus.reasonMessage.locationDetails',
+ {
+ defaultMessage:
+ '{downCount} {downCount, plural, one {time} other {times}} from {locName}',
+ values: {
+ locName: c.ping.observer.geo?.name,
+ downCount: useLatestChecks ? c.checks?.downWithinXChecks : c.checks?.down,
+ },
+ }
+ );
+ })
+ .join(` ${AND_LABEL} `),
+ },
+ }
+ );
+};
+
export const getReasonMessage = ({
name,
status,
location,
- timestamp,
+ checks,
+ params,
}: {
name: string;
location: string;
status: string;
- timestamp: string;
+ checks?: {
+ downWithinXChecks: number;
+ down: number;
+ };
+ params?: StatusRuleParams;
}) => {
- const checkedAt = moment(timestamp).format('LLL');
+ const { useTimeWindow, numberOfChecks, locationsThreshold, downThreshold } = getConditionType(
+ params?.condition
+ );
+ if (useTimeWindow) {
+ return getReasonMessageForTimeWindow({
+ name,
+ location,
+ status,
+ params,
+ });
+ }
+ return i18n.translate('xpack.synthetics.alertRules.monitorStatus.reasonMessage.new', {
+ defaultMessage: `Monitor "{name}" from {location} is {status}. {checksSummary}Alert when {downThreshold} out of the last {numberOfChecks} checks are down from at least {locationsThreshold} {locationsThreshold, plural, one {location} other {locations}}.`,
+ values: {
+ name,
+ status,
+ location,
+ downThreshold,
+ locationsThreshold,
+ numberOfChecks,
+ checksSummary: checks
+ ? i18n.translate('xpack.synthetics.alertRules.monitorStatus.reasonMessage.checksSummary', {
+ defaultMessage:
+ 'Monitor is down {downChecks} {downChecks, plural, one {time} other {times}} within the last {numberOfChecks} checks. ',
+ values: {
+ downChecks: checks.downWithinXChecks,
+ numberOfChecks,
+ },
+ })
+ : '',
+ },
+ });
+};
- return i18n.translate('xpack.synthetics.alertRules.monitorStatus.reasonMessage', {
- defaultMessage: `Monitor "{name}" from {location} is {status}. Checked at {checkedAt}.`,
+export const getReasonMessageForTimeWindow = ({
+ name,
+ location,
+ status = DOWN_LABEL,
+ params,
+}: {
+ name: string;
+ location: string;
+ status?: string;
+ params?: StatusRuleParams;
+}) => {
+ const { timeWindow, locationsThreshold, downThreshold } = getConditionType(params?.condition);
+ return i18n.translate('xpack.synthetics.alertRules.monitorStatus.reasonMessage.timeBased', {
+ defaultMessage: `Monitor "{name}" from {location} is {status}. Alert when {downThreshold} checks are down within the last {size} {unitLabel} from at least {locationsThreshold} {locationsThreshold, plural, one {location} other {locations}}.`,
values: {
name,
status,
location,
- checkedAt,
+ downThreshold,
+ unitLabel: getTimeUnitLabel(timeWindow),
+ locationsThreshold,
+ size: timeWindow.size,
},
});
};
diff --git a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts
index a5d530f2ec53b..ff4516a861225 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts
@@ -7,25 +7,15 @@
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
import { isEmpty } from 'lodash';
-import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common';
-import {
- GetViewInAppRelativeUrlFnOpts,
- AlertInstanceContext as AlertContext,
- RuleExecutorOptions,
- AlertsClientError,
-} from '@kbn/alerting-plugin/server';
-import { getAlertDetailsUrl, observabilityPaths } from '@kbn/observability-plugin/common';
-import { ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils';
+import { GetViewInAppRelativeUrlFnOpts, AlertsClientError } from '@kbn/alerting-plugin/server';
+import { observabilityPaths } from '@kbn/observability-plugin/common';
+import apm from 'elastic-apm-node';
+import { AlertOverviewStatus } from '../../../common/runtime_types/alert_rules/common';
+import { StatusRuleExecutorOptions } from './types';
import { syntheticsRuleFieldMap } from '../../../common/rules/synthetics_rule_field_map';
import { SyntheticsPluginsSetupDependencies, SyntheticsServerSetup } from '../../types';
-import { DOWN_LABEL, getMonitorAlertDocument, getMonitorSummary } from './message_utils';
-import {
- AlertOverviewStatus,
- SyntheticsCommonState,
- SyntheticsMonitorStatusAlertState,
-} from '../../../common/runtime_types/alert_rules/common';
import { StatusRuleExecutor } from './status_rule_executor';
-import { StatusRulePramsSchema, StatusRuleParams } from '../../../common/rules/status_rule';
+import { StatusRulePramsSchema } from '../../../common/rules/status_rule';
import {
MONITOR_STATUS,
SYNTHETICS_ALERT_RULE_TYPES,
@@ -33,22 +23,12 @@ import {
import {
setRecoveredAlertsContext,
updateState,
- getViewInAppUrl,
- getRelativeViewInAppUrl,
- getFullViewInAppMessage,
SyntheticsRuleTypeAlertDefinition,
} from '../common';
-import { ALERT_DETAILS_URL, getActionVariables, VIEW_IN_APP_URL } from '../action_variables';
+import { getActionVariables } from '../action_variables';
import { STATUS_RULE_NAME } from '../translations';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
-type MonitorStatusRuleTypeParams = StatusRuleParams;
-type MonitorStatusActionGroups = ActionGroupIdsOf;
-type MonitorStatusRuleTypeState = SyntheticsCommonState;
-type MonitorStatusAlertState = SyntheticsMonitorStatusAlertState;
-type MonitorStatusAlertContext = AlertContext;
-type MonitorStatusAlert = ObservabilityUptimeAlert;
-
export const registerSyntheticsStatusCheckRule = (
server: SyntheticsServerSetup,
plugins: SyntheticsPluginsSetupDependencies,
@@ -74,94 +54,44 @@ export const registerSyntheticsStatusCheckRule = (
isExportable: true,
minimumLicenseRequired: 'basic',
doesSetRecoveryContext: true,
- executor: async (
- options: RuleExecutorOptions<
- MonitorStatusRuleTypeParams,
- MonitorStatusRuleTypeState,
- MonitorStatusAlertState,
- MonitorStatusAlertContext,
- MonitorStatusActionGroups,
- MonitorStatusAlert
- >
- ) => {
- const { state: ruleState, params, services, spaceId, previousStartedAt, startedAt } = options;
- const { alertsClient, savedObjectsClient, scopedClusterClient, uiSettingsClient } = services;
+ executor: async (options: StatusRuleExecutorOptions) => {
+ apm.setTransactionName('Synthetics Status Rule Executor');
+ const { state: ruleState, params, services, spaceId } = options;
+ const { alertsClient, uiSettingsClient } = services;
if (!alertsClient) {
throw new AlertsClientError();
}
const { basePath } = server;
- const dateFormat = await uiSettingsClient.get('dateFormat');
- const timezone = await uiSettingsClient.get('dateFormat:tz');
+
+ const [dateFormat, timezone] = await Promise.all([
+ uiSettingsClient.get('dateFormat'),
+ uiSettingsClient.get('dateFormat:tz'),
+ ]);
const tz = timezone === 'Browser' ? 'UTC' : timezone;
- const statusRule = new StatusRuleExecutor(
- previousStartedAt,
- params,
- savedObjectsClient,
- scopedClusterClient.asCurrentUser,
- server,
- syntheticsMonitorClient
- );
+ const groupBy = params?.condition?.groupBy ?? 'locationId';
+ const groupByLocation = groupBy === 'locationId';
+
+ const statusRule = new StatusRuleExecutor(server, syntheticsMonitorClient, options);
const { downConfigs, staleDownConfigs, upConfigs } = await statusRule.getDownChecks(
ruleState.meta?.downConfigs as AlertOverviewStatus['downConfigs']
);
- Object.entries(downConfigs).forEach(([idWithLocation, { ping, configId }]) => {
- const locationId = ping.observer.name ?? '';
- const alertId = idWithLocation;
- const monitorSummary = getMonitorSummary(
- ping,
- DOWN_LABEL,
- locationId,
- configId,
- dateFormat,
- tz
- );
-
- const { uuid, start } = alertsClient.report({
- id: alertId,
- actionGroup: MONITOR_STATUS.id,
- });
- const errorStartedAt = start ?? startedAt.toISOString();
-
- let relativeViewInAppUrl = '';
- if (monitorSummary.stateId) {
- relativeViewInAppUrl = getRelativeViewInAppUrl({
- configId,
- stateId: monitorSummary.stateId,
- locationId,
- });
- }
-
- const payload = getMonitorAlertDocument(monitorSummary);
-
- const context = {
- ...monitorSummary,
- idWithLocation,
- errorStartedAt,
- linkMessage: monitorSummary.stateId
- ? getFullViewInAppMessage(basePath, spaceId, relativeViewInAppUrl)
- : '',
- [VIEW_IN_APP_URL]: getViewInAppUrl(basePath, spaceId, relativeViewInAppUrl),
- [ALERT_DETAILS_URL]: getAlertDetailsUrl(basePath, spaceId, uuid),
- };
-
- alertsClient.setAlertData({
- id: alertId,
- payload,
- context,
- });
+ statusRule.handleDownMonitorThresholdAlert({
+ downConfigs,
});
setRecoveredAlertsContext({
alertsClient,
basePath,
spaceId,
- staleDownConfigs,
- upConfigs,
dateFormat,
tz,
+ params,
+ groupByLocation,
+ staleDownConfigs,
+ upConfigs,
});
return {
diff --git a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/queries/filter_monitors.ts b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/queries/filter_monitors.ts
new file mode 100644
index 0000000000000..c850c2b6d6d30
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/queries/filter_monitors.ts
@@ -0,0 +1,104 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
+import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { StatusRuleParams } from '../../../../common/rules/status_rule';
+import { SyntheticsEsClient } from '../../../lib';
+import {
+ FINAL_SUMMARY_FILTER,
+ getRangeFilter,
+ getTimeSpanFilter,
+} from '../../../../common/constants/client_defaults';
+
+export async function queryFilterMonitors({
+ spaceId,
+ esClient,
+ ruleParams,
+}: {
+ spaceId: string;
+ esClient: SyntheticsEsClient;
+ ruleParams: StatusRuleParams;
+}) {
+ if (!ruleParams.kqlQuery) {
+ return;
+ }
+ const filters = toElasticsearchQuery(fromKueryExpression(ruleParams.kqlQuery));
+ const { body: result } = await esClient.search({
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ FINAL_SUMMARY_FILTER,
+ getRangeFilter({ from: 'now-24h/m', to: 'now/m' }),
+ getTimeSpanFilter(),
+ {
+ term: {
+ 'meta.space_id': spaceId,
+ },
+ },
+ {
+ bool: {
+ should: filters,
+ },
+ },
+ ...getFilters(ruleParams),
+ ],
+ },
+ },
+ aggs: {
+ ids: {
+ terms: {
+ size: 10000,
+ field: 'config_id',
+ },
+ },
+ },
+ },
+ });
+
+ return result.aggregations?.ids.buckets.map((bucket) => bucket.key as string);
+}
+
+const getFilters = (ruleParams: StatusRuleParams) => {
+ const { monitorTypes, locations, tags, projects } = ruleParams;
+ const filters: QueryDslQueryContainer[] = [];
+ if (monitorTypes?.length) {
+ filters.push({
+ terms: {
+ 'monitor.type': monitorTypes,
+ },
+ });
+ }
+
+ if (locations?.length) {
+ filters.push({
+ terms: {
+ 'observer.name': locations,
+ },
+ });
+ }
+
+ if (tags?.length) {
+ filters.push({
+ terms: {
+ tags,
+ },
+ });
+ }
+
+ if (projects?.length) {
+ filters.push({
+ terms: {
+ 'monitor.project.id': projects,
+ },
+ });
+ }
+
+ return filters;
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/query_monitor_status_alert.ts b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/queries/query_monitor_status_alert.ts
similarity index 51%
rename from x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/query_monitor_status_alert.ts
rename to x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/queries/query_monitor_status_alert.ts
index 9c8e2fa1fa21b..965c30d9c33ae 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/query_monitor_status_alert.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/queries/query_monitor_status_alert.ts
@@ -8,15 +8,15 @@
import pMap from 'p-map';
import times from 'lodash/times';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-import { cloneDeep, intersection } from 'lodash';
+import { intersection } from 'lodash';
+import { AlertStatusMetaData } from '../../../../common/runtime_types/alert_rules/common';
import {
- AlertOverviewStatus,
- AlertPendingStatusMetaData,
- AlertStatusMetaData,
-} from '../../../common/runtime_types/alert_rules/common';
-import { createEsParams, SyntheticsEsClient } from '../../lib';
-import { OverviewPing } from '../../../common/runtime_types';
-import { FINAL_SUMMARY_FILTER } from '../../../common/constants/client_defaults';
+ FINAL_SUMMARY_FILTER,
+ getTimespanFilter,
+ SUMMARY_FILTER,
+} from '../../../../common/constants/client_defaults';
+import { OverviewPing } from '../../../../common/runtime_types';
+import { createEsParams, SyntheticsEsClient } from '../../../lib';
const DEFAULT_MAX_ES_BUCKET_SIZE = 10000;
@@ -32,23 +32,35 @@ const fields = [
'state',
'tags',
];
+type StatusConfigs = Record;
-export async function queryMonitorStatusForAlert(
- esClient: SyntheticsEsClient,
- monitorLocationIds: string[],
- range: { from: string; to: string },
- monitorQueryIds: string[],
- monitorLocationsMap: Record,
- monitorQueryIdToConfigIdMap: Record
-): Promise {
+export interface AlertStatusResponse {
+ upConfigs: StatusConfigs;
+ downConfigs: StatusConfigs;
+ enabledMonitorQueryIds: string[];
+}
+
+export async function queryMonitorStatusAlert({
+ esClient,
+ monitorLocationIds,
+ range,
+ monitorQueryIds,
+ monitorLocationsMap,
+ numberOfChecks,
+ includeRetests = true,
+}: {
+ esClient: SyntheticsEsClient;
+ monitorLocationIds: string[];
+ range: { from: string; to: string };
+ monitorQueryIds: string[];
+ monitorLocationsMap: Record;
+ numberOfChecks: number;
+ includeRetests?: boolean;
+}): Promise {
const idSize = Math.trunc(DEFAULT_MAX_ES_BUCKET_SIZE / monitorLocationIds.length || 1);
const pageCount = Math.ceil(monitorQueryIds.length / idSize);
- let up = 0;
- let down = 0;
- const upConfigs: Record = {};
- const downConfigs: Record = {};
- const monitorsWithoutData = new Map(Object.entries(cloneDeep(monitorLocationsMap)));
- const pendingConfigs: Record = {};
+ const upConfigs: StatusConfigs = {};
+ const downConfigs: StatusConfigs = {};
await pMap(
times(pageCount),
@@ -60,15 +72,8 @@ export async function queryMonitorStatusForAlert(
query: {
bool: {
filter: [
- FINAL_SUMMARY_FILTER,
- {
- range: {
- '@timestamp': {
- gte: range.from,
- lte: range.to,
- },
- },
- },
+ ...(includeRetests ? [SUMMARY_FILTER] : [FINAL_SUMMARY_FILTER]),
+ getTimespanFilter({ from: range.from, to: range.to }),
{
terms: {
'monitor.id': idsToQuery,
@@ -90,9 +95,18 @@ export async function queryMonitorStatusForAlert(
size: monitorLocationIds.length || 100,
},
aggs: {
- status: {
+ downChecks: {
+ filter: {
+ range: {
+ 'summary.down': {
+ gte: '1',
+ },
+ },
+ },
+ },
+ totalChecks: {
top_hits: {
- size: 1,
+ size: numberOfChecks,
sort: [
{
'@timestamp': {
@@ -121,62 +135,60 @@ export async function queryMonitorStatusForAlert(
});
}
- const { body: result } = await esClient.search(
- params,
- 'getCurrentStatusOverview' + i
- );
+ const { body: result } = await esClient.search(params);
result.aggregations?.id.buckets.forEach(({ location, key: queryId }) => {
- const locationSummaries = location.buckets.map(({ status, key: locationName }) => {
- const ping = status.hits.hits[0]._source;
- return { location: locationName, ping };
- });
+ const locationSummaries = location.buckets.map(
+ ({ key: locationId, totalChecks, downChecks }) => {
+ return { locationId, totalChecks, downChecks };
+ }
+ );
// discard any locations that are not in the monitorLocationsMap for the given monitor as well as those which are
// in monitorLocationsMap but not in listOfLocations
const monLocations = monitorLocationsMap?.[queryId];
const monQueriedLocations = intersection(monLocations, monitorLocationIds);
- monQueriedLocations?.forEach((monLocation) => {
+ monQueriedLocations?.forEach((monLocationId) => {
const locationSummary = locationSummaries.find(
- (summary) => summary.location === monLocation
+ (summary) => summary.locationId === monLocationId
);
if (locationSummary) {
- const { ping } = locationSummary;
- const downCount = ping.summary?.down ?? 0;
- const upCount = ping.summary?.up ?? 0;
- const configId = ping.config_id;
- const monitorQueryId = ping.monitor.id;
+ const { totalChecks, downChecks } = locationSummary;
+ const latestPing = totalChecks.hits.hits[0]._source;
+ const downCount = downChecks.doc_count;
+ const isLatestPingUp = (latestPing.summary?.up ?? 0) > 0;
+ const configId = latestPing.config_id;
+ const monitorQueryId = latestPing.monitor.id;
- const meta = {
- ping,
+ const meta: AlertStatusMetaData = {
+ ping: latestPing,
configId,
monitorQueryId,
- locationId: monLocation,
- timestamp: ping['@timestamp'],
+ locationId: monLocationId,
+ timestamp: latestPing['@timestamp'],
+ checks: {
+ downWithinXChecks: totalChecks.hits.hits.reduce(
+ (acc, curr) => acc + ((curr._source.summary.down ?? 0) > 0 ? 1 : 0),
+ 0
+ ),
+ down: downCount,
+ },
+ status: 'up',
};
if (downCount > 0) {
- down += 1;
- downConfigs[`${configId}-${monLocation}`] = {
+ downConfigs[`${configId}-${monLocationId}`] = {
...meta,
status: 'down',
};
- } else if (upCount > 0) {
- up += 1;
- upConfigs[`${configId}-${monLocation}`] = {
+ }
+ if (isLatestPingUp) {
+ upConfigs[`${configId}-${monLocationId}`] = {
...meta,
status: 'up',
};
}
- const monitorsMissingData = monitorsWithoutData.get(monitorQueryId) || [];
- monitorsWithoutData.set(
- monitorQueryId,
- monitorsMissingData?.filter((loc) => loc !== monLocation)
- );
- if (!monitorsWithoutData.get(monitorQueryId)?.length) {
- monitorsWithoutData.delete(monitorQueryId);
- }
}
});
});
@@ -184,26 +196,9 @@ export async function queryMonitorStatusForAlert(
{ concurrency: 5 }
);
- // identify the remaining monitors without data, to determine pending monitors
- for (const [queryId, locs] of monitorsWithoutData) {
- locs.forEach((loc) => {
- pendingConfigs[`${monitorQueryIdToConfigIdMap[queryId]}-${loc}`] = {
- configId: `${monitorQueryIdToConfigIdMap[queryId]}`,
- monitorQueryId: queryId,
- status: 'unknown',
- locationId: loc,
- };
- });
- }
-
return {
- up,
- down,
- pending: Object.values(pendingConfigs).length,
upConfigs,
downConfigs,
- pendingConfigs,
enabledMonitorQueryIds: monitorQueryIds,
- staleDownConfigs: {},
};
}
diff --git a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts
index 02373c34ac6a5..76c05d6fa2930 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts
@@ -4,10 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import moment from 'moment';
import { loggerMock } from '@kbn/logging-mocks';
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
-import { StatusRuleExecutor } from './status_rule_executor';
+import { coreMock } from '@kbn/core/server/mocks';
+import { getDoesMonitorMeetLocationThreshold, StatusRuleExecutor } from './status_rule_executor';
import { mockEncryptedSO } from '../../synthetics_service/utils/mocks';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
@@ -16,8 +16,12 @@ import * as monitorUtils from '../../saved_objects/synthetics_monitor/get_all_mo
import * as locationsUtils from '../../synthetics_service/get_all_locations';
import type { PublicLocation } from '../../../common/runtime_types';
import { SyntheticsServerSetup } from '../../types';
+import { AlertStatusMetaData } from '../../../common/runtime_types/alert_rules/common';
describe('StatusRuleExecutor', () => {
+ // @ts-ignore
+ Date.now = jest.fn(() => new Date('2024-05-13T12:33:37.000Z'));
+
const mockEsClient = elasticsearchClientMock.createElasticsearchClient();
const logger = loggerMock.create();
const soClient = savedObjectsClientMock.create();
@@ -59,166 +63,611 @@ describe('StatusRuleExecutor', () => {
const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
- it('should only query enabled monitors', async () => {
- const spy = jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([]);
- const statusRule = new StatusRuleExecutor(
- moment().toDate(),
- {},
- soClient,
- mockEsClient,
- serverMock,
- monitorClient
- );
- const { downConfigs, staleDownConfigs } = await statusRule.getDownChecks({});
-
- expect(downConfigs).toEqual({});
- expect(staleDownConfigs).toEqual({});
-
- expect(spy).toHaveBeenCalledWith({
- filter: 'synthetics-monitor.attributes.alert.status.enabled: true',
- soClient,
+ const mockStart = coreMock.createStart();
+ const uiSettingsClient = mockStart.uiSettings.asScopedToClient(soClient);
+
+ const statusRule = new StatusRuleExecutor(serverMock, monitorClient, {
+ params: {},
+ services: {
+ uiSettingsClient,
+ savedObjectsClient: soClient,
+ scopedClusterClient: { asCurrentUser: mockEsClient },
+ },
+ rule: {
+ name: 'test',
+ },
+ } as any);
+
+ describe('DefaultRule', () => {
+ it('should only query enabled monitors', async () => {
+ const spy = jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([]);
+
+ const { downConfigs, staleDownConfigs } = await statusRule.getDownChecks({});
+
+ expect(downConfigs).toEqual({});
+ expect(staleDownConfigs).toEqual({});
+
+ expect(spy).toHaveBeenCalledWith({
+ filter: 'synthetics-monitor.attributes.alert.status.enabled: true',
+ soClient,
+ });
});
- });
- it('marks deleted configs as expected', async () => {
- jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue(testMonitors);
- const statusRule = new StatusRuleExecutor(
- moment().toDate(),
- {},
- soClient,
- mockEsClient,
- serverMock,
- monitorClient
- );
-
- const { downConfigs } = await statusRule.getDownChecks({});
-
- expect(downConfigs).toEqual({});
-
- const staleDownConfigs = await statusRule.markDeletedConfigs({
- id1: {
- locationId: 'us-east-1',
- configId: 'id1',
- status: 'down',
- timestamp: '2021-06-01T00:00:00.000Z',
- monitorQueryId: 'test',
- ping: {} as any,
- },
- '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': {
- locationId: 'us_central_dev',
- configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
- status: 'down',
- timestamp: '2021-06-01T00:00:00.000Z',
- monitorQueryId: 'test',
- ping: {} as any,
- },
- '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_qa': {
- locationId: 'us_central_qa',
- configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
- status: 'down',
- timestamp: '2021-06-01T00:00:00.000Z',
- monitorQueryId: 'test',
- ping: {} as any,
- },
+ it('marks deleted configs as expected', async () => {
+ jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue(testMonitors);
+
+ const { downConfigs } = await statusRule.getDownChecks({});
+
+ expect(downConfigs).toEqual({});
+
+ const staleDownConfigs = await statusRule.markDeletedConfigs({
+ id2: {
+ locationId: 'us-east-1',
+ configId: 'id2',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: {} as any,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ 'id1-us_central_dev': {
+ locationId: 'us_central_dev',
+ configId: 'id1',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: {} as any,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ 'id1-us_central_qa': {
+ locationId: 'us_central_qa',
+ configId: 'id1',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: {} as any,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ });
+
+ expect(staleDownConfigs).toEqual({
+ id2: {
+ configId: 'id2',
+ isDeleted: true,
+ locationId: 'us-east-1',
+ monitorQueryId: 'test',
+ ping: {},
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ 'id1-us_central_dev': {
+ configId: 'id1',
+ isLocationRemoved: true,
+ locationId: 'us_central_dev',
+ monitorQueryId: 'test',
+ ping: {},
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ });
});
- expect(staleDownConfigs).toEqual({
- id1: {
- configId: 'id1',
- isDeleted: true,
- locationId: 'us-east-1',
- monitorQueryId: 'test',
- ping: {},
- status: 'down',
- timestamp: '2021-06-01T00:00:00.000Z',
- },
- '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': {
- configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
- isLocationRemoved: true,
- locationId: 'us_central_dev',
- monitorQueryId: 'test',
- ping: {},
- status: 'down',
- timestamp: '2021-06-01T00:00:00.000Z',
- },
+ it('does not mark deleted config when monitor does not contain location label', async () => {
+ jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([
+ {
+ ...testMonitors[0],
+ attributes: {
+ ...testMonitors[0].attributes,
+ locations: [
+ {
+ geo: { lon: -95.86, lat: 41.25 },
+ isServiceManaged: true,
+ id: 'us_central_qa',
+ },
+ ],
+ },
+ },
+ ]);
+
+ const { downConfigs } = await statusRule.getDownChecks({});
+
+ expect(downConfigs).toEqual({});
+
+ const staleDownConfigs = await statusRule.markDeletedConfigs({
+ id2: {
+ locationId: 'us-east-1',
+ configId: 'id2',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: {} as any,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ 'id1-us_central_dev': {
+ locationId: 'us_central_dev',
+ configId: 'id1',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: {} as any,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ 'id1-us_central_qa': {
+ locationId: 'us_central_qa',
+ configId: 'id1',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: {} as any,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ });
+
+ expect(staleDownConfigs).toEqual({
+ id2: {
+ configId: 'id2',
+ isDeleted: true,
+ locationId: 'us-east-1',
+ monitorQueryId: 'test',
+ ping: {},
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ 'id1-us_central_dev': {
+ configId: 'id1',
+ isLocationRemoved: true,
+ locationId: 'us_central_dev',
+ monitorQueryId: 'test',
+ ping: {},
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ });
});
});
- it('does not mark deleted config when monitor does not contain location label', async () => {
- jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([
- {
- ...testMonitors[0],
- attributes: {
- ...testMonitors[0].attributes,
- locations: [
- {
- geo: { lon: -95.86, lat: 41.25 },
- isServiceManaged: true,
- id: 'us_central_qa',
+ describe('handleDownMonitorThresholdAlert', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should alert if monitor meet location threshold', async () => {
+ const spy = jest.spyOn(statusRule, 'scheduleAlert');
+ statusRule.handleDownMonitorThresholdAlert({
+ downConfigs: {
+ 'id1-us_central_qa': {
+ locationId: 'us_central_qa',
+ configId: 'id1',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: testPing,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
},
- ],
+ },
},
- },
- ]);
- const statusRule = new StatusRuleExecutor(
- moment().toDate(),
- {},
- soClient,
- mockEsClient,
- serverMock,
- monitorClient
- );
-
- const { downConfigs } = await statusRule.getDownChecks({});
-
- expect(downConfigs).toEqual({});
-
- const staleDownConfigs = await statusRule.markDeletedConfigs({
- id1: {
- locationId: 'us-east-1',
- configId: 'id1',
- status: 'down',
- timestamp: '2021-06-01T00:00:00.000Z',
- monitorQueryId: 'test',
- ping: {} as any,
- },
- '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': {
- locationId: 'us_central_dev',
- configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
- status: 'down',
- timestamp: '2021-06-01T00:00:00.000Z',
- monitorQueryId: 'test',
- ping: {} as any,
- },
- '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_qa': {
- locationId: 'us_central_qa',
- configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
- status: 'down',
- timestamp: '2021-06-01T00:00:00.000Z',
- monitorQueryId: 'test',
- ping: {} as any,
- },
+ });
+ expect(spy).toHaveBeenCalledWith({
+ alertId: 'id1-us_central_qa',
+ downThreshold: 1,
+ idWithLocation: 'id1-us_central_qa',
+ locationNames: ['Test location'],
+ locationIds: ['test'],
+ monitorSummary: {
+ checkedAt: '2024-05-13T12:33:37Z',
+ checks: { down: 1, downWithinXChecks: 1 },
+ configId: 'id1',
+ downThreshold: 1,
+ hostName: undefined,
+ lastErrorMessage: undefined,
+ locationId: 'us_central_qa',
+ locationName: 'Test location',
+ locationNames: 'Test location',
+ monitorId: 'test',
+ monitorName: 'test monitor',
+ monitorTags: ['dev'],
+ monitorType: 'browser',
+ monitorUrl: 'https://www.google.com',
+ monitorUrlLabel: 'URL',
+ reason:
+ 'Monitor "test monitor" from Test location is down. Monitor is down 1 time within the last 1 checks. Alert when 1 out of the last 1 checks are down from at least 1 location.',
+ stateId: undefined,
+ status: 'down',
+ timestamp: '2024-05-13T12:33:37.000Z',
+ },
+ statusConfig: {
+ checks: { down: 1, downWithinXChecks: 1 },
+ configId: 'id1',
+ locationId: 'us_central_qa',
+ monitorQueryId: 'test',
+ ping: testPing,
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ useLatestChecks: true,
+ });
});
- expect(staleDownConfigs).toEqual({
- id1: {
- configId: 'id1',
- isDeleted: true,
- locationId: 'us-east-1',
- monitorQueryId: 'test',
- ping: {},
- status: 'down',
- timestamp: '2021-06-01T00:00:00.000Z',
- },
- '2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': {
- configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
- isLocationRemoved: true,
- locationId: 'us_central_dev',
- monitorQueryId: 'test',
- ping: {},
- status: 'down',
- timestamp: '2021-06-01T00:00:00.000Z',
- },
+ it('should not alert if monitor do not meet location threshold', async () => {
+ statusRule.params = {
+ condition: {
+ window: {
+ numberOfChecks: 1,
+ },
+ downThreshold: 1,
+ locationsThreshold: 2,
+ },
+ };
+
+ const spy = jest.spyOn(statusRule, 'scheduleAlert');
+ statusRule.handleDownMonitorThresholdAlert({
+ downConfigs: {
+ 'id1-us_central_qa': {
+ locationId: 'us_central_qa',
+ configId: 'id1',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: testPing,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ },
+ });
+ expect(spy).toHaveBeenCalledTimes(0);
+ });
+
+ it('should send 2 alerts', async () => {
+ statusRule.params = {
+ condition: {
+ window: {
+ numberOfChecks: 1,
+ },
+ downThreshold: 1,
+ locationsThreshold: 1,
+ },
+ };
+ const spy = jest.spyOn(statusRule, 'scheduleAlert');
+ statusRule.handleDownMonitorThresholdAlert({
+ downConfigs: {
+ 'id1-us_central_qa': {
+ locationId: 'us_central_qa',
+ configId: 'id1',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: testPing,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ 'id1-us_central_dev': {
+ locationId: 'us_central_dev',
+ configId: 'id1',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: testPing,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ },
+ });
+ expect(spy).toHaveBeenCalledTimes(2);
+ });
+
+ it('should send 1 alert for un-grouped', async () => {
+ statusRule.params = {
+ condition: {
+ groupBy: 'none',
+ window: {
+ numberOfChecks: 1,
+ },
+ downThreshold: 1,
+ locationsThreshold: 1,
+ },
+ };
+ const spy = jest.spyOn(statusRule, 'scheduleAlert');
+ statusRule.handleDownMonitorThresholdAlert({
+ downConfigs: {
+ 'id1-us_central_qa': {
+ locationId: 'us_central_qa',
+ configId: 'id1',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: testPing,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ 'id1-us_central_dev': {
+ locationId: 'us_central_dev',
+ configId: 'id1',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ monitorQueryId: 'test',
+ ping: testPing,
+ checks: {
+ downWithinXChecks: 1,
+ down: 1,
+ },
+ },
+ },
+ });
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledWith({
+ alertId: 'id1',
+ downThreshold: 1,
+ idWithLocation: 'id1',
+ locationIds: ['test', 'test'],
+ locationNames: ['Test location', 'Test location'],
+ monitorSummary: {
+ checkedAt: '2024-05-13T12:33:37Z',
+ checks: { down: 1, downWithinXChecks: 1 },
+ configId: 'id1',
+ downThreshold: 1,
+ hostName: undefined,
+ lastErrorMessage: undefined,
+ locationId: 'test and test',
+ locationName: 'Test location',
+ locationNames: 'Test location and Test location',
+ monitorId: 'test',
+ monitorName: 'test monitor',
+ monitorTags: ['dev'],
+ monitorType: 'browser',
+ monitorUrl: 'https://www.google.com',
+ monitorUrlLabel: 'URL',
+ reason:
+ 'Monitor "test monitor" is down 1 time from Test location and 1 time from Test location. Alert when down 1 time out of the last 1 checks from at least 1 location.',
+ status: 'down',
+ timestamp: '2024-05-13T12:33:37.000Z',
+ },
+ statusConfig: {
+ checks: { down: 1, downWithinXChecks: 1 },
+ configId: 'id1',
+ locationId: 'us_central_qa',
+ monitorQueryId: 'test',
+ ping: testPing,
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ useLatestChecks: true,
+ });
+ });
+ });
+});
+
+describe('getDoesMonitorMeetLocationThreshold', () => {
+ describe('when useTimeWindow is false', () => {
+ it('should return false if monitor does not meets location threshold', () => {
+ const matchesByLocation: AlertStatusMetaData[] = [
+ {
+ checks: { down: 0, downWithinXChecks: 0 },
+ locationId: 'us_central_qa',
+ ping: testPing,
+ configId: 'id1',
+ monitorQueryId: 'test',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ ];
+ const res = getDoesMonitorMeetLocationThreshold({
+ matchesByLocation,
+ locationsThreshold: 1,
+ downThreshold: 1,
+ useTimeWindow: false,
+ });
+ expect(res).toBe(false);
+ });
+
+ it('should return true if monitor meets location threshold', () => {
+ const matchesByLocation: AlertStatusMetaData[] = [
+ {
+ checks: { down: 1, downWithinXChecks: 1 },
+ locationId: 'us_central_qa',
+ ping: testPing,
+ configId: 'id1',
+ monitorQueryId: 'test',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ ];
+ const res = getDoesMonitorMeetLocationThreshold({
+ matchesByLocation,
+ locationsThreshold: 1,
+ downThreshold: 1,
+ useTimeWindow: false,
+ });
+ expect(res).toBe(true);
+ });
+
+ it('should return false if monitor does not meets 2 location threshold', () => {
+ const matchesByLocation: AlertStatusMetaData[] = [
+ {
+ checks: { down: 1, downWithinXChecks: 1 },
+ locationId: 'us_central_qa',
+ ping: testPing,
+ configId: 'id1',
+ monitorQueryId: 'test',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ ];
+ const res = getDoesMonitorMeetLocationThreshold({
+ matchesByLocation,
+ locationsThreshold: 2,
+ downThreshold: 1,
+ useTimeWindow: false,
+ });
+ expect(res).toBe(false);
+ });
+
+ it('should return true if monitor meets 2 location threshold', () => {
+ const matchesByLocation: AlertStatusMetaData[] = [
+ {
+ checks: { down: 1, downWithinXChecks: 1 },
+ locationId: 'us_central_qa',
+ ping: testPing,
+ configId: 'id1',
+ monitorQueryId: 'test',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ {
+ checks: { down: 1, downWithinXChecks: 1 },
+ locationId: 'us_central',
+ ping: testPing,
+ configId: 'id1',
+ monitorQueryId: 'test',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ ];
+ const res = getDoesMonitorMeetLocationThreshold({
+ matchesByLocation,
+ locationsThreshold: 2,
+ downThreshold: 1,
+ useTimeWindow: false,
+ });
+ expect(res).toBe(true);
+ });
+ });
+
+ describe('when useTimeWindow is true', () => {
+ it('should return false if monitor does not meets location threshold', () => {
+ const matchesByLocation: AlertStatusMetaData[] = [
+ {
+ checks: { down: 0, downWithinXChecks: 0 },
+ locationId: 'us_central_qa',
+ ping: testPing,
+ configId: 'id1',
+ monitorQueryId: 'test',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ ];
+ const res = getDoesMonitorMeetLocationThreshold({
+ matchesByLocation,
+ locationsThreshold: 1,
+ downThreshold: 1,
+ useTimeWindow: true,
+ });
+ expect(res).toBe(false);
+ });
+
+ it('should return true if monitor meets location threshold', () => {
+ const matchesByLocation: AlertStatusMetaData[] = [
+ {
+ checks: { down: 1, downWithinXChecks: 0 },
+ locationId: 'us_central_qa',
+ ping: testPing,
+ configId: 'id1',
+ monitorQueryId: 'test',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ ];
+ const res = getDoesMonitorMeetLocationThreshold({
+ matchesByLocation,
+ locationsThreshold: 1,
+ downThreshold: 1,
+ useTimeWindow: true,
+ });
+ expect(res).toBe(true);
+ });
+
+ it('should return false if monitor does not meets 2 location threshold', () => {
+ const matchesByLocation: AlertStatusMetaData[] = [
+ {
+ checks: { down: 1, downWithinXChecks: 0 },
+ locationId: 'us_central_qa',
+ ping: testPing,
+ configId: 'id1',
+ monitorQueryId: 'test',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ ];
+ const res = getDoesMonitorMeetLocationThreshold({
+ matchesByLocation,
+ locationsThreshold: 2,
+ downThreshold: 1,
+ useTimeWindow: true,
+ });
+ expect(res).toBe(false);
+ });
+
+ it('should return true if monitor meets 2 location threshold', () => {
+ const matchesByLocation: AlertStatusMetaData[] = [
+ {
+ checks: { down: 1, downWithinXChecks: 0 },
+ locationId: 'us_central_qa',
+ ping: testPing,
+ configId: 'id1',
+ monitorQueryId: 'test',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ {
+ checks: { down: 1, downWithinXChecks: 1 },
+ locationId: 'us_central',
+ ping: testPing,
+ configId: 'id1',
+ monitorQueryId: 'test',
+ status: 'down',
+ timestamp: '2021-06-01T00:00:00.000Z',
+ },
+ ];
+ const res = getDoesMonitorMeetLocationThreshold({
+ matchesByLocation,
+ locationsThreshold: 2,
+ downThreshold: 1,
+ useTimeWindow: true,
+ });
+ expect(res).toBe(true);
});
});
});
@@ -226,7 +675,7 @@ describe('StatusRuleExecutor', () => {
const testMonitors = [
{
type: 'synthetics-monitor',
- id: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
+ id: 'id1',
attributes: {
type: 'browser',
form_monitor_type: 'multistep',
@@ -234,7 +683,7 @@ const testMonitors = [
alert: { status: { enabled: false } },
schedule: { unit: 'm', number: '10' },
'service.name': '',
- config_id: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
+ config_id: 'id1',
tags: [],
timeout: null,
name: 'https://www.google.com',
@@ -250,7 +699,7 @@ const testMonitors = [
origin: 'ui',
journey_id: '',
hash: '',
- id: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
+ id: 'id1',
project_id: '',
playwright_options: '',
__ui: {
@@ -289,3 +738,22 @@ const testMonitors = [
sort: ['https://www.google.com', 1889],
},
] as any;
+
+const testPing = {
+ '@timestamp': '2024-05-13T12:33:37.000Z',
+ monitor: {
+ id: 'test',
+ name: 'test monitor',
+ type: 'browser',
+ },
+ tags: ['dev'],
+ url: {
+ full: 'https://www.google.com',
+ },
+ observer: {
+ name: 'test',
+ geo: {
+ name: 'Test location',
+ },
+ },
+} as any;
diff --git a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/status_rule_executor.ts b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/status_rule_executor.ts
index 691176fad6e74..7dcea3a6084f0 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/status_rule_executor.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/status_rule_executor.ts
@@ -9,9 +9,31 @@ import {
SavedObjectsClientContract,
SavedObjectsFindResult,
} from '@kbn/core-saved-objects-api-server';
-import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
-import { AlertOverviewStatus } from '../../../common/runtime_types/alert_rules/common';
-import { queryMonitorStatusForAlert } from './query_monitor_status_alert';
+import { Logger } from '@kbn/core/server';
+import { intersection, isEmpty, uniq } from 'lodash';
+import { getAlertDetailsUrl } from '@kbn/observability-plugin/common';
+import {
+ AlertOverviewStatus,
+ AlertStatusConfigs,
+ AlertStatusMetaData,
+ StaleDownConfig,
+} from '../../../common/runtime_types/alert_rules/common';
+import { queryFilterMonitors } from './queries/filter_monitors';
+import { MonitorSummaryStatusRule, StatusRuleExecutorOptions } from './types';
+import {
+ AND_LABEL,
+ getFullViewInAppMessage,
+ getRelativeViewInAppUrl,
+ getViewInAppUrl,
+} from '../common';
+import {
+ DOWN_LABEL,
+ getMonitorAlertDocument,
+ getMonitorSummary,
+ getUngroupedReasonMessage,
+} from './message_utils';
+import { queryMonitorStatusAlert } from './queries/query_monitor_status_alert';
+import { parseArrayFilters } from '../../routes/common';
import { SyntheticsServerSetup } from '../../types';
import { SyntheticsEsClient } from '../../lib';
import { SYNTHETICS_INDEX_PATTERN } from '../../../common/constants';
@@ -19,11 +41,13 @@ import {
getAllMonitors,
processMonitors,
} from '../../saved_objects/synthetics_monitor/get_all_monitors';
-import { StatusRuleParams } from '../../../common/rules/status_rule';
+import { getConditionType, StatusRuleParams } from '../../../common/rules/status_rule';
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { monitorAttributes } from '../../../common/types/saved_objects';
import { AlertConfigKey } from '../../../common/constants/monitor_management';
+import { ALERT_DETAILS_URL, VIEW_IN_APP_URL } from '../action_variables';
+import { MONITOR_STATUS } from '../../../common/constants/synthetics_alerts';
export class StatusRuleExecutor {
previousStartedAt: Date | null;
@@ -33,106 +57,176 @@ export class StatusRuleExecutor {
server: SyntheticsServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
monitors: Array> = [];
+ hasCustomCondition: boolean;
+ monitorLocationsMap: Record; // monitorId: locationIds
+ dateFormat?: string;
+ tz?: string;
+ options: StatusRuleExecutorOptions;
+ logger: Logger;
+ ruleName: string;
constructor(
- previousStartedAt: Date | null,
- p: StatusRuleParams,
- soClient: SavedObjectsClientContract,
- scopedClient: ElasticsearchClient,
server: SyntheticsServerSetup,
- syntheticsMonitorClient: SyntheticsMonitorClient
+ syntheticsMonitorClient: SyntheticsMonitorClient,
+ options: StatusRuleExecutorOptions
) {
+ const { services, params, previousStartedAt, rule } = options;
+ const { scopedClusterClient, savedObjectsClient } = services;
+ this.ruleName = rule.name;
+ this.logger = server.logger;
this.previousStartedAt = previousStartedAt;
- this.params = p;
- this.soClient = soClient;
- this.esClient = new SyntheticsEsClient(this.soClient, scopedClient, {
+ this.params = params;
+ this.soClient = savedObjectsClient;
+ this.esClient = new SyntheticsEsClient(this.soClient, scopedClusterClient.asCurrentUser, {
heartbeatIndices: SYNTHETICS_INDEX_PATTERN,
});
this.server = server;
this.syntheticsMonitorClient = syntheticsMonitorClient;
+ this.hasCustomCondition = !isEmpty(this.params);
+ this.monitorLocationsMap = {};
+ this.options = options;
+ }
+
+ debug(message: string) {
+ this.logger.debug(`[Status Rule Executor][${this.ruleName}] ${message}`);
+ }
+
+ async init() {
+ const { uiSettingsClient } = this.options.services;
+ this.dateFormat = await uiSettingsClient.get('dateFormat');
+ const timezone = await uiSettingsClient.get('dateFormat:tz');
+ this.tz = timezone === 'Browser' ? 'UTC' : timezone;
}
async getMonitors() {
+ const baseFilter = !this.hasCustomCondition
+ ? `${monitorAttributes}.${AlertConfigKey.STATUS_ENABLED}: true`
+ : '';
+
+ const configIds = await queryFilterMonitors({
+ spaceId: this.options.spaceId,
+ esClient: this.esClient,
+ ruleParams: this.params,
+ });
+
+ const { filtersStr } = parseArrayFilters({
+ configIds,
+ filter: baseFilter,
+ tags: this.params?.tags,
+ locations: this.params?.locations,
+ monitorTypes: this.params?.monitorTypes,
+ monitorQueryIds: this.params?.monitorIds,
+ projects: this.params?.projects,
+ });
+
this.monitors = await getAllMonitors({
soClient: this.soClient,
- filter: `${monitorAttributes}.${AlertConfigKey.STATUS_ENABLED}: true`,
+ filter: filtersStr,
});
- const {
- allIds,
- enabledMonitorQueryIds,
- monitorLocationIds,
- monitorLocationsMap,
- projectMonitorsCount,
- monitorQueryIdToConfigIdMap,
- } = processMonitors(this.monitors);
-
- return {
- enabledMonitorQueryIds,
- monitorLocationIds,
- allIds,
- monitorLocationsMap,
- projectMonitorsCount,
- monitorQueryIdToConfigIdMap,
- };
+ this.debug(`Found ${this.monitors.length} monitors for params ${JSON.stringify(this.params)}`);
+ return processMonitors(this.monitors);
}
- async getDownChecks(
- prevDownConfigs: AlertOverviewStatus['downConfigs'] = {}
- ): Promise {
- const {
- monitorLocationIds,
- enabledMonitorQueryIds,
- monitorLocationsMap,
- monitorQueryIdToConfigIdMap,
- } = await this.getMonitors();
- const from = this.previousStartedAt
- ? moment(this.previousStartedAt).subtract(1, 'minute').toISOString()
- : 'now-2m';
+ async getDownChecks(prevDownConfigs: AlertStatusConfigs = {}): Promise {
+ await this.init();
+ const { enabledMonitorQueryIds, maxPeriod, monitorLocationIds, monitorLocationsMap } =
+ await this.getMonitors();
- if (enabledMonitorQueryIds.length > 0) {
- const currentStatus = await queryMonitorStatusForAlert(
- this.esClient,
- monitorLocationIds,
- {
- to: 'now',
- from,
- },
- enabledMonitorQueryIds,
- monitorLocationsMap,
- monitorQueryIdToConfigIdMap
- );
+ const range = this.getRange(maxPeriod);
- const downConfigs = currentStatus.downConfigs;
- const upConfigs = currentStatus.upConfigs;
-
- Object.keys(prevDownConfigs).forEach((locId) => {
- if (!downConfigs[locId] && !upConfigs[locId]) {
- downConfigs[locId] = prevDownConfigs[locId];
- }
- });
-
- const staleDownConfigs = this.markDeletedConfigs(downConfigs);
+ const { numberOfChecks } = getConditionType(this.params.condition);
+ if (enabledMonitorQueryIds.length === 0) {
+ const staleDownConfigs = this.markDeletedConfigs(prevDownConfigs);
return {
- ...currentStatus,
+ downConfigs: { ...prevDownConfigs },
+ upConfigs: {},
staleDownConfigs,
+ enabledMonitorQueryIds,
+ pendingConfigs: {},
};
}
- const staleDownConfigs = this.markDeletedConfigs(prevDownConfigs);
+
+ const queryLocations = this.params?.locations;
+
+ // Account for locations filter
+ const listOfLocationAfterFilter = queryLocations
+ ? intersection(monitorLocationIds, queryLocations)
+ : monitorLocationIds;
+
+ const currentStatus = await queryMonitorStatusAlert({
+ esClient: this.esClient,
+ monitorLocationIds: listOfLocationAfterFilter,
+ range,
+ monitorQueryIds: enabledMonitorQueryIds,
+ numberOfChecks,
+ monitorLocationsMap,
+ includeRetests: this.params.condition?.includeRetests,
+ });
+
+ const { downConfigs, upConfigs } = currentStatus;
+
+ this.debug(
+ `Found ${Object.keys(downConfigs).length} down configs and ${
+ Object.keys(upConfigs).length
+ } up configs`
+ );
+
+ const downConfigsById = getConfigsByIds(downConfigs);
+ const upConfigsById = getConfigsByIds(upConfigs);
+
+ uniq([...downConfigsById.keys(), ...upConfigsById.keys()]).forEach((configId) => {
+ const downCount = downConfigsById.get(configId)?.length ?? 0;
+ const upCount = upConfigsById.get(configId)?.length ?? 0;
+ const name = this.monitors.find((m) => m.id === configId)?.attributes.name ?? configId;
+ this.debug(
+ `Monitor: ${name} with id ${configId} has ${downCount} down check and ${upCount} up check`
+ );
+ });
+
+ Object.keys(prevDownConfigs).forEach((locId) => {
+ if (!downConfigs[locId] && !upConfigs[locId]) {
+ downConfigs[locId] = prevDownConfigs[locId];
+ }
+ });
+
+ const staleDownConfigs = this.markDeletedConfigs(downConfigs);
+
return {
- downConfigs: { ...prevDownConfigs },
- upConfigs: {},
- pendingConfigs: {},
+ ...currentStatus,
staleDownConfigs,
- down: 0,
- up: 0,
- pending: 0,
- enabledMonitorQueryIds,
+ pendingConfigs: {},
};
}
- markDeletedConfigs(downConfigs: AlertOverviewStatus['downConfigs']) {
+ getRange = (maxPeriod: number) => {
+ let from = this.previousStartedAt
+ ? moment(this.previousStartedAt).subtract(1, 'minute').toISOString()
+ : 'now-2m';
+
+ const condition = this.params.condition;
+ if (condition && 'numberOfChecks' in condition?.window) {
+ const numberOfChecks = condition.window.numberOfChecks;
+ from = moment()
+ .subtract(maxPeriod * numberOfChecks, 'milliseconds')
+ .subtract(5, 'minutes')
+ .toISOString();
+ } else if (condition && 'time' in condition.window) {
+ const time = condition.window.time;
+ const { unit, size } = time;
+
+ from = moment().subtract(size, unit).toISOString();
+ }
+
+ this.debug(
+ `Using range from ${from} to now, diff of ${moment().diff(from, 'minutes')} minutes`
+ );
+
+ return { from, to: 'now' };
+ };
+
+ markDeletedConfigs(downConfigs: AlertStatusConfigs): Record {
const monitors = this.monitors;
const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {};
Object.keys(downConfigs).forEach((locPlusId) => {
@@ -158,4 +252,221 @@ export class StatusRuleExecutor {
return staleDownConfigs;
}
+
+ handleDownMonitorThresholdAlert = ({ downConfigs }: { downConfigs: AlertStatusConfigs }) => {
+ const { useTimeWindow, useLatestChecks, downThreshold, locationsThreshold } = getConditionType(
+ this.params?.condition
+ );
+ const groupBy = this.params?.condition?.groupBy ?? 'locationId';
+
+ if (groupBy === 'locationId' && locationsThreshold === 1) {
+ Object.entries(downConfigs).forEach(([idWithLocation, statusConfig]) => {
+ const doesMonitorMeetLocationThreshold = getDoesMonitorMeetLocationThreshold({
+ matchesByLocation: [statusConfig],
+ locationsThreshold,
+ downThreshold,
+ useTimeWindow: useTimeWindow || false,
+ });
+ if (doesMonitorMeetLocationThreshold) {
+ const alertId = idWithLocation;
+ const monitorSummary = this.getMonitorDownSummary({
+ statusConfig,
+ });
+
+ return this.scheduleAlert({
+ idWithLocation,
+ alertId,
+ monitorSummary,
+ statusConfig,
+ downThreshold,
+ useLatestChecks,
+ locationNames: [statusConfig.ping.observer.geo?.name!],
+ locationIds: [statusConfig.ping.observer.name!],
+ });
+ }
+ });
+ } else {
+ const downConfigsById = getConfigsByIds(downConfigs);
+
+ for (const [configId, configs] of downConfigsById) {
+ const doesMonitorMeetLocationThreshold = getDoesMonitorMeetLocationThreshold({
+ matchesByLocation: configs,
+ locationsThreshold,
+ downThreshold,
+ useTimeWindow: useTimeWindow || false,
+ });
+
+ if (doesMonitorMeetLocationThreshold) {
+ const alertId = configId;
+ const monitorSummary = this.getUngroupedDownSummary({
+ statusConfigs: configs,
+ });
+ return this.scheduleAlert({
+ idWithLocation: configId,
+ alertId,
+ monitorSummary,
+ statusConfig: configs[0],
+ downThreshold,
+ useLatestChecks,
+ locationNames: configs.map((c) => c.ping.observer.geo?.name!),
+ locationIds: configs.map((c) => c.ping.observer.name!),
+ });
+ }
+ }
+ }
+ };
+
+ getMonitorDownSummary({ statusConfig }: { statusConfig: AlertStatusMetaData }) {
+ const { ping, configId, locationId, checks } = statusConfig;
+
+ return getMonitorSummary({
+ monitorInfo: ping,
+ statusMessage: DOWN_LABEL,
+ locationId: [locationId],
+ configId,
+ dateFormat: this.dateFormat ?? 'Y-MM-DD HH:mm:ss',
+ tz: this.tz ?? 'UTC',
+ checks,
+ params: this.params,
+ });
+ }
+
+ getUngroupedDownSummary({ statusConfigs }: { statusConfigs: AlertStatusMetaData[] }) {
+ const sampleConfig = statusConfigs[0];
+ const { ping, configId, checks } = sampleConfig;
+ const baseSummary = getMonitorSummary({
+ monitorInfo: ping,
+ statusMessage: DOWN_LABEL,
+ locationId: statusConfigs.map((c) => c.ping.observer.name!),
+ configId,
+ dateFormat: this.dateFormat!,
+ tz: this.tz!,
+ checks,
+ params: this.params,
+ });
+ baseSummary.reason = getUngroupedReasonMessage({
+ statusConfigs,
+ monitorName: baseSummary.monitorName,
+ params: this.params,
+ });
+ if (statusConfigs.length > 1) {
+ baseSummary.locationNames = statusConfigs
+ .map((c) => c.ping.observer.geo?.name!)
+ .join(` ${AND_LABEL} `);
+ }
+
+ return baseSummary;
+ }
+
+ scheduleAlert({
+ idWithLocation,
+ alertId,
+ monitorSummary,
+ statusConfig,
+ downThreshold,
+ useLatestChecks = false,
+ locationNames,
+ locationIds,
+ }: {
+ idWithLocation: string;
+ alertId: string;
+ monitorSummary: MonitorSummaryStatusRule;
+ statusConfig: AlertStatusMetaData;
+ downThreshold: number;
+ useLatestChecks?: boolean;
+ locationNames: string[];
+ locationIds: string[];
+ }) {
+ const { configId, locationId, checks } = statusConfig;
+ const { spaceId, startedAt } = this.options;
+ const { alertsClient } = this.options.services;
+ const { basePath } = this.server;
+ if (!alertsClient) return;
+
+ const { uuid: alertUuid, start } = alertsClient.report({
+ id: alertId,
+ actionGroup: MONITOR_STATUS.id,
+ });
+ const errorStartedAt = start ?? startedAt.toISOString() ?? monitorSummary.timestamp;
+
+ let relativeViewInAppUrl = '';
+ if (monitorSummary.stateId) {
+ relativeViewInAppUrl = getRelativeViewInAppUrl({
+ configId,
+ locationId,
+ stateId: monitorSummary.stateId,
+ });
+ }
+
+ const context = {
+ ...monitorSummary,
+ idWithLocation,
+ checks,
+ downThreshold,
+ errorStartedAt,
+ linkMessage: monitorSummary.stateId
+ ? getFullViewInAppMessage(basePath, spaceId, relativeViewInAppUrl)
+ : '',
+ [VIEW_IN_APP_URL]: getViewInAppUrl(basePath, spaceId, relativeViewInAppUrl),
+ [ALERT_DETAILS_URL]: getAlertDetailsUrl(basePath, spaceId, alertUuid),
+ };
+
+ const alertDocument = getMonitorAlertDocument(
+ monitorSummary,
+ locationNames,
+ locationIds,
+ useLatestChecks
+ );
+
+ alertsClient.setAlertData({
+ id: alertId,
+ payload: alertDocument,
+ context,
+ });
+ }
}
+
+export const getDoesMonitorMeetLocationThreshold = ({
+ matchesByLocation,
+ locationsThreshold,
+ downThreshold,
+ useTimeWindow,
+}: {
+ matchesByLocation: AlertStatusMetaData[];
+ locationsThreshold: number;
+ downThreshold: number;
+ useTimeWindow: boolean;
+}) => {
+ // for location based we need to make sure, monitor is down for the threshold for all locations
+ const getMatchingLocationsWithDownThresholdWithXChecks = (matches: AlertStatusMetaData[]) => {
+ return matches.filter((config) => (config.checks?.downWithinXChecks ?? 1) >= downThreshold);
+ };
+ const getMatchingLocationsWithDownThresholdWithinTimeWindow = (
+ matches: AlertStatusMetaData[]
+ ) => {
+ return matches.filter((config) => (config.checks?.down ?? 1) >= downThreshold);
+ };
+ if (useTimeWindow) {
+ const matchingLocationsWithDownThreshold =
+ getMatchingLocationsWithDownThresholdWithinTimeWindow(matchesByLocation);
+ return matchingLocationsWithDownThreshold.length >= locationsThreshold;
+ } else {
+ const matchingLocationsWithDownThreshold =
+ getMatchingLocationsWithDownThresholdWithXChecks(matchesByLocation);
+ return matchingLocationsWithDownThreshold.length >= locationsThreshold;
+ }
+};
+
+export const getConfigsByIds = (
+ downConfigs: AlertStatusConfigs
+): Map => {
+ const downConfigsById = new Map();
+ Object.entries(downConfigs).forEach(([_, config]) => {
+ const { configId } = config;
+ if (!downConfigsById.has(configId)) {
+ downConfigsById.set(configId, []);
+ }
+ downConfigsById.get(configId)?.push(config);
+ });
+ return downConfigsById;
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/types.ts b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/types.ts
index 16f8318b04f2e..3190881d728c4 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/types.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/types.ts
@@ -5,6 +5,47 @@
* 2.0.
*/
+import { ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils';
+import { ActionGroupIdsOf } from '@kbn/alerting-types';
+import {
+ AlertInstanceContext as AlertContext,
+ RuleExecutorOptions,
+} from '@kbn/alerting-plugin/server';
+import { StatusRuleParams } from '../../../common/rules/status_rule';
+import { MONITOR_STATUS } from '../../../common/constants/synthetics_alerts';
+import {
+ SyntheticsCommonState,
+ SyntheticsMonitorStatusAlertState,
+} from '../../../common/runtime_types/alert_rules/common';
+
+type MonitorStatusRuleTypeParams = StatusRuleParams;
+type MonitorStatusActionGroups = ActionGroupIdsOf;
+type MonitorStatusRuleTypeState = SyntheticsCommonState;
+type MonitorStatusAlertState = SyntheticsMonitorStatusAlertState;
+type MonitorStatusAlertContext = AlertContext;
+
+export type StatusRuleExecutorOptions = RuleExecutorOptions<
+ MonitorStatusRuleTypeParams,
+ MonitorStatusRuleTypeState,
+ MonitorStatusAlertState,
+ MonitorStatusAlertContext,
+ MonitorStatusActionGroups,
+ MonitorStatusAlertDocument
+>;
+
+export type MonitorStatusAlertDocument = ObservabilityUptimeAlert &
+ Required<
+ Pick<
+ ObservabilityUptimeAlert,
+ | 'monitor.id'
+ | 'monitor.type'
+ | 'monitor.name'
+ | 'configId'
+ | 'observer.geo.name'
+ | 'location.name'
+ | 'location.id'
+ >
+ >;
export interface MonitorSummaryStatusRule {
reason: string;
status: string;
@@ -17,8 +58,15 @@ export interface MonitorSummaryStatusRule {
monitorType: string;
monitorName: string;
locationName: string;
- lastErrorMessage: string;
- stateId: string | null;
+ locationNames: string;
monitorUrlLabel: string;
monitorTags?: string[];
+ downThreshold: number;
+ checks?: {
+ downWithinXChecks: number;
+ down: number;
+ };
+ stateId?: string;
+ lastErrorMessage?: string;
+ timestamp: string;
}
diff --git a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/utils.ts b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/utils.ts
new file mode 100644
index 0000000000000..608526c3b39e3
--- /dev/null
+++ b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/status_rule/utils.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import moment from 'moment';
+import { EncryptedSyntheticsMonitorAttributes, OverviewPing } from '../../../common/runtime_types';
+
+export const getMonitorToPing = (
+ monitor: EncryptedSyntheticsMonitorAttributes,
+ locationId: string
+) => {
+ const location = monitor.locations.find((loc) => loc.id === locationId);
+ return {
+ monitor: {
+ id: monitor.id,
+ name: monitor.name,
+ type: monitor.type,
+ },
+ observer: {
+ name: location?.id,
+ geo: {
+ name: location?.label,
+ },
+ },
+ config_id: monitor.config_id,
+ } as OverviewPing;
+};
+
+export const getIntervalFromTimespan = (timespan: { gte: string; lt: string }) => {
+ const start = moment(timespan.gte);
+ const end = moment(timespan.lt);
+ return end.diff(start, 'seconds');
+};
diff --git a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/translations.ts b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/translations.ts
index 5dced6f8928e5..37a4cd1e12e97 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/alert_rules/translations.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/alert_rules/translations.ts
@@ -88,6 +88,15 @@ export const commonMonitorStateI18: Array<{
}
),
},
+ {
+ name: 'locationNames',
+ description: i18n.translate(
+ 'xpack.synthetics.alertRules.monitorStatus.actionVariables.state.locationNames',
+ {
+ defaultMessage: 'Location names from which the checks are performed.',
+ }
+ ),
+ },
{
name: 'locationId',
description: i18n.translate(
diff --git a/x-pack/plugins/observability_solution/synthetics/server/lib.ts b/x-pack/plugins/observability_solution/synthetics/server/lib.ts
index 22f5661ca532f..94150c0fb8ee5 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/lib.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/lib.ts
@@ -90,7 +90,7 @@ export class SyntheticsEsClient {
esRequestStatus = RequestStatus.ERROR;
}
const isInspectorEnabled = await this.getInspectEnabled();
- if (isInspectorEnabled && this.request) {
+ if ((isInspectorEnabled || this.isDev) && this.request) {
this.inspectableEsQueries.push(
getInspectResponse({
esError,
@@ -102,7 +102,9 @@ export class SyntheticsEsClient {
startTime: startTimeNow,
})
);
+ }
+ if (isInspectorEnabled && this.request) {
debugESCall({
startTime,
request: this.request,
@@ -218,9 +220,6 @@ export class SyntheticsEsClient {
return {};
}
async getInspectEnabled() {
- if (this.isDev) {
- return true;
- }
if (!this.uiSettings) {
return false;
}
diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/common.test.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/common.test.ts
index 82520c68b5fc4..7e5c0d610e520 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/routes/common.test.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/routes/common.test.ts
@@ -5,8 +5,43 @@
* 2.0.
*/
+import { parseArrayFilters } from './common';
import { getSavedObjectKqlFilter } from './common';
+describe('common utils', () => {
+ it('tests parseArrayFilters', () => {
+ const filters = parseArrayFilters({
+ configIds: ['1 4', '2 6', '5'],
+ });
+ expect(filters.filtersStr).toMatchInlineSnapshot(
+ `"synthetics-monitor.attributes.config_id:(\\"1 4\\" OR \\"2 6\\" OR \\"5\\")"`
+ );
+ });
+ it('tests parseArrayFilters with tags and configIds', () => {
+ const filters = parseArrayFilters({
+ configIds: ['1', '2'],
+ tags: ['tag1', 'tag2'],
+ });
+ expect(filters.filtersStr).toMatchInlineSnapshot(
+ `"synthetics-monitor.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor.attributes.config_id:(\\"1\\" OR \\"2\\")"`
+ );
+ });
+ it('tests parseArrayFilters with all options', () => {
+ const filters = parseArrayFilters({
+ configIds: ['1', '2'],
+ tags: ['tag1', 'tag2'],
+ locations: ['loc1', 'loc2'],
+ monitorTypes: ['type1', 'type2'],
+ projects: ['project1', 'project2'],
+ monitorQueryIds: ['query1', 'query2'],
+ schedules: ['schedule1', 'schedule2'],
+ });
+ expect(filters.filtersStr).toMatchInlineSnapshot(
+ `"synthetics-monitor.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor.attributes.project_id:(\\"project1\\" OR \\"project2\\") AND synthetics-monitor.attributes.type:(\\"type1\\" OR \\"type2\\") AND synthetics-monitor.attributes.schedule.number:(\\"schedule1\\" OR \\"schedule2\\") AND synthetics-monitor.attributes.id:(\\"query1\\" OR \\"query2\\") AND synthetics-monitor.attributes.config_id:(\\"1\\" OR \\"2\\")"`
+ );
+ });
+});
+
describe('getSavedObjectKqlFilter', () => {
it('returns empty string if no values are provided', () => {
expect(getSavedObjectKqlFilter({ field: 'tags' })).toBe('');
diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts
index bc58a866bef83..2edb17a77a635 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts
@@ -7,6 +7,7 @@
import { schema, TypeOf } from '@kbn/config-schema';
import { SavedObjectsFindResponse } from '@kbn/core/server';
+import { isEmpty } from 'lodash';
import { escapeQuotes } from '@kbn/es-query';
import { RouteContext } from './types';
import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_management/sort_field';
@@ -110,16 +111,7 @@ export const getMonitors = async (
return context.savedObjectsClient.find(findParams);
};
-export const getMonitorFilters = async ({
- tags,
- filter,
- locations,
- projects,
- monitorTypes,
- schedules,
- monitorQueryIds,
- context,
-}: {
+interface Filters {
filter?: string;
tags?: string | string[];
monitorTypes?: string | string[];
@@ -127,10 +119,35 @@ export const getMonitorFilters = async ({
projects?: string | string[];
schedules?: string | string[];
monitorQueryIds?: string | string[];
- context: RouteContext;
-}) => {
+}
+
+export const getMonitorFilters = async (
+ data: {
+ context: RouteContext;
+ } & Filters
+) => {
+ const { context, locations } = data;
const locationFilter = await parseLocationFilter(context, locations);
+ return parseArrayFilters({
+ ...data,
+ locationFilter,
+ });
+};
+
+export const parseArrayFilters = ({
+ tags,
+ filter,
+ configIds,
+ projects,
+ monitorTypes,
+ schedules,
+ monitorQueryIds,
+ locationFilter,
+}: Filters & {
+ locationFilter?: string | string[];
+ configIds?: string[];
+}) => {
const filtersStr = [
filter,
getSavedObjectKqlFilter({ field: 'tags', values: tags }),
@@ -139,9 +156,11 @@ export const getMonitorFilters = async ({
getSavedObjectKqlFilter({ field: 'locations.id', values: locationFilter }),
getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules }),
getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds }),
+ getSavedObjectKqlFilter({ field: 'config_id', values: configIds }),
]
.filter((f) => !!f)
.join(' AND ');
+
return { filtersStr, locationFilter };
};
@@ -156,7 +175,11 @@ export const getSavedObjectKqlFilter = ({
operator?: string;
searchAtRoot?: boolean;
}) => {
- if (!values) {
+ if (values === 'All' || (Array.isArray(values) && values?.includes('All'))) {
+ return undefined;
+ }
+
+ if (isEmpty(values) || !values) {
return '';
}
let fieldKey = '';
diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_service_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_service_locations.ts
index ca45e74f89004..0f6b398fb6b68 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_service_locations.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_service_locations.ts
@@ -17,21 +17,32 @@ import {
LocationStatus,
} from '../../common/runtime_types';
-export const getDevLocation = (devUrl: string): PublicLocation => ({
- id: 'dev',
- label: 'Dev Service',
- geo: { lat: 0, lon: 0 },
- url: devUrl,
- isServiceManaged: true,
- status: LocationStatus.EXPERIMENTAL,
- isInvalid: false,
-});
+export const getDevLocation = (devUrl: string): PublicLocation[] => [
+ {
+ id: 'dev',
+ label: 'Dev Service',
+ geo: { lat: 0, lon: 0 },
+ url: devUrl,
+ isServiceManaged: true,
+ status: LocationStatus.EXPERIMENTAL,
+ isInvalid: false,
+ },
+ {
+ id: 'dev2',
+ label: 'Dev Service 2',
+ geo: { lat: 0, lon: 0 },
+ url: devUrl,
+ isServiceManaged: true,
+ status: LocationStatus.EXPERIMENTAL,
+ isInvalid: false,
+ },
+];
export async function getServiceLocations(server: SyntheticsServerSetup) {
let locations: PublicLocations = [];
if (server.config.service?.devUrl) {
- locations = [getDevLocation(server.config.service.devUrl)];
+ locations = getDevLocation(server.config.service.devUrl);
}
const manifestUrl = server.config.service?.manifestUrl;
diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/synthetics_service.test.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/synthetics_service.test.ts
index 40f392084942b..fdc41831e8afd 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/synthetics_service.test.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/synthetics_service.test.ts
@@ -200,6 +200,18 @@ describe('SyntheticsService', () => {
isServiceManaged: true,
status: LocationStatus.EXPERIMENTAL,
},
+ {
+ geo: {
+ lat: 0,
+ lon: 0,
+ },
+ id: 'dev2',
+ isInvalid: false,
+ isServiceManaged: true,
+ label: 'Dev Service 2',
+ status: 'experimental',
+ url: 'http://localhost',
+ },
]);
});
diff --git a/x-pack/plugins/observability_solution/synthetics/tsconfig.json b/x-pack/plugins/observability_solution/synthetics/tsconfig.json
index d0822a733baff..24411ebdcb0c5 100644
--- a/x-pack/plugins/observability_solution/synthetics/tsconfig.json
+++ b/x-pack/plugins/observability_solution/synthetics/tsconfig.json
@@ -98,6 +98,10 @@
"@kbn/presentation-util-plugin",
"@kbn/core-application-browser",
"@kbn/dashboard-plugin",
+ "@kbn/search-types",
+ "@kbn/slo-schema",
+ "@kbn/alerting-types",
+ "@kbn/babel-register",
"@kbn/slo-plugin",
"@kbn/ebt-tools",
"@kbn/alerting-types"
diff --git a/x-pack/plugins/observability_solution/uptime/common/rules/uptime_rule_field_map.ts b/x-pack/plugins/observability_solution/uptime/common/rules/uptime_rule_field_map.ts
index ca6463416de6b..3b2da879c5288 100644
--- a/x-pack/plugins/observability_solution/uptime/common/rules/uptime_rule_field_map.ts
+++ b/x-pack/plugins/observability_solution/uptime/common/rules/uptime_rule_field_map.ts
@@ -17,8 +17,14 @@ export const uptimeRuleFieldMap: FieldMap = {
type: 'keyword',
required: false,
},
+ 'observer.name': {
+ type: 'keyword',
+ array: true,
+ required: false,
+ },
'observer.geo.name': {
type: 'keyword',
+ array: true,
required: false,
},
// monitor status alert fields
@@ -43,6 +49,10 @@ export const uptimeRuleFieldMap: FieldMap = {
array: true,
required: false,
},
+ 'monitor.state.id': {
+ type: 'keyword',
+ required: false,
+ },
configId: {
type: 'keyword',
required: false,
@@ -53,10 +63,12 @@ export const uptimeRuleFieldMap: FieldMap = {
},
'location.id': {
type: 'keyword',
+ array: true,
required: false,
},
'location.name': {
type: 'keyword',
+ array: true,
required: false,
},
// tls alert fields
diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.test.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.test.ts
index 13d959000c3a8..6054866c7b608 100644
--- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.test.ts
+++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.test.ts
@@ -113,7 +113,10 @@ const mockCommonAlertDocumentFields = (monitorInfo: GetMonitorStatusResult['moni
'monitor.name': monitorInfo.monitor.name || monitorInfo.monitor.id,
'monitor.type': monitorInfo.monitor.type,
'url.full': monitorInfo.url?.full,
- 'observer.geo.name': monitorInfo.observer?.geo?.name,
+ 'observer.geo.name': monitorInfo.observer?.geo?.name
+ ? [monitorInfo.observer.geo.name]
+ : undefined,
+ 'observer.name': [],
});
const mockStatusAlertDocument = (
diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.ts
index ea6edf35b6f12..0ce64bc803821 100644
--- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.ts
+++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.ts
@@ -213,8 +213,8 @@ export const getMonitorAlertDocument = (monitorSummary: MonitorSummary) => ({
'monitor.name': monitorSummary.monitorName,
'monitor.tags': monitorSummary.tags,
'url.full': monitorSummary.monitorUrl,
- 'observer.geo.name': monitorSummary.observerLocation,
- 'observer.name': monitorSummary.observerName,
+ 'observer.geo.name': [monitorSummary.observerLocation],
+ 'observer.name': [monitorSummary.observerName!],
'error.message': monitorSummary.latestErrorMessage,
'agent.name': monitorSummary.observerHostname,
[ALERT_REASON]: monitorSummary.reason,
diff --git a/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx b/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx
index ddf4f27122ef9..22735cc86c05f 100644
--- a/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx
+++ b/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types';
-import { Result, resultToField, resultMetaData } from '@kbn/search-index-documents';
+import { Result, resultMetaData, resultToField } from '@kbn/search-index-documents';
import { EuiSpacer } from '@elastic/eui';
diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/alibaba_cloud_ai_search.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/alibaba_cloud_ai_search.svg
new file mode 100644
index 0000000000000..18534e2d0b3a1
--- /dev/null
+++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/alibaba_cloud_ai_search.svg
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.test.tsx
index bcb6599632177..202e914e33a35 100644
--- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.test.tsx
+++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.test.tsx
@@ -268,4 +268,24 @@ describe('ServiceProvider component', () => {
expect(screen.getByText('model-bedrock-xyz')).toBeInTheDocument();
});
});
+
+ describe('with alibabacloud-ai-search service', () => {
+ const mockEndpoint = {
+ inference_id: 'alibabacloud-ai-search-1',
+ service: 'alibabacloud-ai-search',
+ service_settings: {
+ service_id: 'service-123',
+ host: 'host-123',
+ workspace: 'default-123',
+ },
+ } as any;
+
+ it('renders the component with endpoint details', () => {
+ render( );
+
+ expect(screen.getByText('AlibabaCloud AI Search')).toBeInTheDocument();
+ const icon = screen.getByTestId('table-column-service-provider-alibabacloud-ai-search');
+ expect(icon).toBeInTheDocument();
+ });
+ });
});
diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.tsx
index b926f590335fb..574b3881f121b 100644
--- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.tsx
+++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.tsx
@@ -20,6 +20,7 @@ import azureOpenAIIcon from '../../../../assets/images/providers/azure_open_ai.s
import googleAIStudioIcon from '../../../../assets/images/providers/google_ai_studio.svg';
import mistralIcon from '../../../../assets/images/providers/mistral.svg';
import amazonBedrockIcon from '../../../../assets/images/providers/amazon_bedrock.svg';
+import alibabaCloudAISearchIcon from '../../../../assets/images/providers/alibaba_cloud_ai_search.svg';
import { ServiceProviderKeys } from '../../types';
import * as i18n from './translations';
@@ -33,6 +34,10 @@ interface ServiceProviderRecord {
}
export const SERVICE_PROVIDERS: Record = {
+ [ServiceProviderKeys['alibabacloud-ai-search']]: {
+ icon: alibabaCloudAISearchIcon,
+ name: 'AlibabaCloud AI Search',
+ },
[ServiceProviderKeys.amazonbedrock]: {
icon: amazonBedrockIcon,
name: 'Amazon Bedrock',
diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts
index 0a5da7288607d..6fb4cb0bcca6b 100644
--- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts
+++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts
@@ -10,6 +10,7 @@ import { TaskTypes } from '../../types';
export const INFERENCE_ENDPOINTS_TABLE_PER_PAGE_VALUES = [25, 50, 100];
export enum ServiceProviderKeys {
+ 'alibabacloud-ai-search' = 'alibabacloud-ai-search',
amazonbedrock = 'amazonbedrock',
azureopenai = 'azureopenai',
azureaistudio = 'azureaistudio',
diff --git a/x-pack/plugins/search_playground/common/types.ts b/x-pack/plugins/search_playground/common/types.ts
index c0e3300fe7dff..c239858b5b459 100644
--- a/x-pack/plugins/search_playground/common/types.ts
+++ b/x-pack/plugins/search_playground/common/types.ts
@@ -51,6 +51,7 @@ export enum APIRoutes {
POST_QUERY_SOURCE_FIELDS = '/internal/search_playground/query_source_fields',
GET_INDICES = '/internal/search_playground/indices',
POST_SEARCH_QUERY = '/internal/search_playground/search',
+ GET_INDEX_MAPPINGS = '/internal/search_playground/mappings',
}
export enum LLMs {
diff --git a/x-pack/plugins/search_playground/public/components/app.tsx b/x-pack/plugins/search_playground/public/components/app.tsx
index 34b89433ea705..4f371ea5d15bb 100644
--- a/x-pack/plugins/search_playground/public/components/app.tsx
+++ b/x-pack/plugins/search_playground/public/components/app.tsx
@@ -106,7 +106,11 @@ export const App: React.FC = ({
css={{
position: 'relative',
}}
- contentProps={{ css: { display: 'flex', flexGrow: 1, position: 'absolute', inset: 0 } }}
+ contentProps={
+ selectedPageMode === PlaygroundPageMode.search && selectedMode === 'chat'
+ ? undefined
+ : { css: { display: 'flex', flexGrow: 1, position: 'absolute', inset: 0 } }
+ }
paddingSize={paddingSize}
className="eui-fullHeight"
>
diff --git a/x-pack/plugins/search_playground/public/components/search_mode/empty_results.tsx b/x-pack/plugins/search_playground/public/components/search_mode/empty_results.tsx
deleted file mode 100644
index ab5779e85ddd5..0000000000000
--- a/x-pack/plugins/search_playground/public/components/search_mode/empty_results.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-import { EuiEmptyPrompt } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-
-export interface EmptyResultsArgs {
- query?: string;
-}
-
-export const EmptyResults: React.FC = ({ query }) => {
- return (
-
- {query
- ? i18n.translate('xpack.searchPlayground.resultList.emptyWithQuery.text', {
- defaultMessage: 'No result found for: {query}',
- values: { query },
- })
- : i18n.translate('xpack.searchPlayground.resultList.empty.text', {
- defaultMessage: 'No results found',
- })}
-
- }
- />
- );
-};
diff --git a/x-pack/plugins/search_playground/public/components/search_mode/result_list.tsx b/x-pack/plugins/search_playground/public/components/search_mode/result_list.tsx
index 02e1193e22332..87c7060c29151 100644
--- a/x-pack/plugins/search_playground/public/components/search_mode/result_list.tsx
+++ b/x-pack/plugins/search_playground/public/components/search_mode/result_list.tsx
@@ -7,40 +7,29 @@
import React, { useEffect, useState } from 'react';
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiHorizontalRule,
- EuiPagination,
- EuiPanel,
- EuiText,
- EuiTitle,
-} from '@elastic/eui';
-
-import { UnifiedDocViewerFlyout } from '@kbn/unified-doc-viewer-plugin/public';
+import { DocumentList, pageToPagination } from '@kbn/search-index-documents';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { EsHitRecord } from '@kbn/discover-utils/types';
-import type { SearchHit } from '@elastic/elasticsearch/lib/api/types';
+import type { IndicesGetMappingResponse, SearchHit } from '@elastic/elasticsearch/lib/api/types';
import { buildDataTableRecord } from '@kbn/discover-utils';
-import { i18n } from '@kbn/i18n';
-import { Pagination } from '../../types';
-import { getPageCounts } from '../../utils/pagination_helper';
-import { EmptyResults } from './empty_results';
+import { UnifiedDocViewerFlyout } from '@kbn/unified-doc-viewer-plugin/public';
+import { Pagination as PaginationTypeEui } from '@elastic/eui';
import { useKibana } from '../../hooks/use_kibana';
+import { Pagination } from '../../types';
export interface ResultListArgs {
searchResults: SearchHit[];
+ mappings?: IndicesGetMappingResponse;
pagination: Pagination;
onPaginationChange: (nextPage: number) => void;
- searchQuery?: string;
}
export const ResultList: React.FC = ({
searchResults,
+ mappings,
pagination,
onPaginationChange,
- searchQuery = '',
}) => {
const {
services: { data },
@@ -50,73 +39,42 @@ export const ResultList: React.FC = ({
data.dataViews.getDefaultDataView().then((d) => setDataView(d));
}, [data]);
const [flyoutDocId, setFlyoutDocId] = useState(undefined);
- const { totalPage, page } = getPageCounts(pagination);
+ const documentMeta: PaginationTypeEui = pageToPagination(pagination);
const hit =
flyoutDocId &&
buildDataTableRecord(searchResults.find((item) => item._id === flyoutDocId) as EsHitRecord);
return (
-
-
- {searchResults.length === 0 && (
-
-
-
- )}
- {searchResults.length !== 0 &&
- searchResults.map((item, index) => {
- return (
- <>
- setFlyoutDocId(item._id)}
- grow
- >
-
-
-
- ID:{item._id}
-
-
-
-
-
- {i18n.translate('xpack.searchPlayground.resultList.result.score', {
- defaultMessage: 'Document score: {score}',
- values: { score: item._score },
- })}
-
-
-
-
-
- {index !== searchResults.length - 1 && }
- >
- );
- })}
- {searchResults.length !== 0 && (
-
-
-
- )}
- {flyoutDocId && dataView && hit && (
- setFlyoutDocId(undefined)}
- isEsqlQuery={false}
- columns={[]}
- hit={hit}
- dataView={dataView}
- onAddColumn={() => {}}
- onRemoveColumn={() => {}}
- setExpandedDoc={() => {}}
- flyoutType="overlay"
- />
- )}
-
-
+ <>
+ setFlyoutDocId(searchHit._id)}
+ resultProps={{
+ showScore: true,
+ compactCard: false,
+ defaultVisibleFields: 0,
+ }}
+ />
+
+ {flyoutDocId && dataView && hit && (
+ setFlyoutDocId(undefined)}
+ isEsqlQuery={false}
+ columns={[]}
+ hit={hit}
+ dataView={dataView}
+ onAddColumn={() => {}}
+ onRemoveColumn={() => {}}
+ setExpandedDoc={() => {}}
+ flyoutType="overlay"
+ />
+ )}
+ >
);
};
diff --git a/x-pack/plugins/search_playground/public/components/search_mode/search_mode.tsx b/x-pack/plugins/search_playground/public/components/search_mode/search_mode.tsx
index 967c5786eed63..94e6337f1a03c 100644
--- a/x-pack/plugins/search_playground/public/components/search_mode/search_mode.tsx
+++ b/x-pack/plugins/search_playground/public/components/search_mode/search_mode.tsx
@@ -23,6 +23,7 @@ import { ResultList } from './result_list';
import { ChatForm, ChatFormFields, Pagination } from '../../types';
import { useSearchPreview } from '../../hooks/use_search_preview';
import { getPaginationFromPage } from '../../utils/pagination_helper';
+import { useIndexMappings } from '../../hooks/use_index_mappings';
export const SearchMode: React.FC = () => {
const { euiTheme } = useEuiTheme();
@@ -40,6 +41,7 @@ export const SearchMode: React.FC = () => {
}>({ query: searchBarValue, pagination: DEFAULT_PAGINATION });
const { results, pagination } = useSearchPreview(searchQuery);
+ const { data: mappingData } = useIndexMappings();
const queryClient = useQueryClient();
const handleSearch = async (query = searchBarValue, paginationParam = DEFAULT_PAGINATION) => {
@@ -81,15 +83,15 @@ export const SearchMode: React.FC = () => {
/>
-
+
{searchQuery.query ? (
) : (
{
+ const mappings = await http.post<{
+ mappings: IndicesGetMappingResponse;
+ }>(APIRoutes.GET_INDEX_MAPPINGS, {
+ body: JSON.stringify({
+ indices,
+ }),
+ });
+ return mappings;
+};
+export const useIndexMappings = () => {
+ const {
+ services: { http },
+ } = useKibana();
+ const { getValues } = useFormContext();
+ const indices = getValues(ChatFormFields.indices);
+ const { data } = useQuery({
+ queryKey: ['search-playground-index-mappings'],
+ queryFn: () => fetchIndexMappings({ indices, http }),
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ });
+
+ return { data: data?.mappings };
+};
diff --git a/x-pack/plugins/search_playground/public/hooks/use_search_preview.ts b/x-pack/plugins/search_playground/public/hooks/use_search_preview.ts
index 54566563fcee5..f66f81b37cd2e 100644
--- a/x-pack/plugins/search_playground/public/hooks/use_search_preview.ts
+++ b/x-pack/plugins/search_playground/public/hooks/use_search_preview.ts
@@ -8,6 +8,7 @@
import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
import { useQuery } from '@tanstack/react-query';
import { useFormContext } from 'react-hook-form';
+import type { HttpSetup } from '@kbn/core-http-browser';
import { APIRoutes, ChatForm, ChatFormFields, Pagination } from '../types';
import { useKibana } from './use_kibana';
import { DEFAULT_PAGINATION } from '../../common';
@@ -17,7 +18,7 @@ export interface FetchSearchResultsArgs {
pagination: Pagination;
indices: ChatForm[ChatFormFields.indices];
elasticsearchQuery: ChatForm[ChatFormFields.elasticsearchQuery];
- http: ReturnType['services']['http'];
+ http: HttpSetup;
}
interface UseSearchPreviewData {
@@ -64,9 +65,10 @@ export const useSearchPreview = ({
query: string;
pagination: Pagination;
}) => {
- const { services } = useKibana();
+ const {
+ services: { http },
+ } = useKibana();
const { getValues } = useFormContext();
- const { http } = services;
const indices = getValues(ChatFormFields.indices);
const elasticsearchQuery = getValues(ChatFormFields.elasticsearchQuery);
diff --git a/x-pack/plugins/search_playground/public/plugin.ts b/x-pack/plugins/search_playground/public/plugin.ts
index 20142c807b609..47bc8352b763e 100644
--- a/x-pack/plugins/search_playground/public/plugin.ts
+++ b/x-pack/plugins/search_playground/public/plugin.ts
@@ -47,6 +47,9 @@ export class SearchPlaygroundPlugin
async mount({ element, history }: AppMountParameters) {
const { renderApp } = await import('./application');
const [coreStart, depsStart] = await core.getStartServices();
+
+ coreStart.chrome.docTitle.change(PLUGIN_NAME);
+
const startDeps: AppPluginStartDependencies = {
...depsStart,
history,
diff --git a/x-pack/plugins/search_playground/public/utils/pagination_helper.ts b/x-pack/plugins/search_playground/public/utils/pagination_helper.ts
index 1379bbc257bd4..2602327b8c968 100644
--- a/x-pack/plugins/search_playground/public/utils/pagination_helper.ts
+++ b/x-pack/plugins/search_playground/public/utils/pagination_helper.ts
@@ -7,13 +7,6 @@
import { Pagination } from '../../common/types';
-export const getPageCounts = (pagination: Pagination) => {
- const { total, from, size } = pagination;
- const totalPage = Math.ceil(total / size);
- const page = Math.floor(from / size);
- return { totalPage, total, page, size };
-};
-
export const getPaginationFromPage = (page: number, size: number, previousValue: Pagination) => {
const from = page < 0 ? 0 : page * size;
return { ...previousValue, from, size, page };
diff --git a/x-pack/plugins/search_playground/server/routes.ts b/x-pack/plugins/search_playground/server/routes.ts
index d30904214d8df..cf6a139b1344b 100644
--- a/x-pack/plugins/search_playground/server/routes.ts
+++ b/x-pack/plugins/search_playground/server/routes.ts
@@ -305,4 +305,47 @@ export function defineRoutes({
}
})
);
+ router.post(
+ {
+ path: APIRoutes.GET_INDEX_MAPPINGS,
+ validate: {
+ body: schema.object({
+ indices: schema.arrayOf(schema.string()),
+ }),
+ },
+ },
+ errorHandler(logger)(async (context, request, response) => {
+ const { client } = (await context.core).elasticsearch;
+ const { indices } = request.body;
+
+ try {
+ if (indices.length === 0) {
+ return response.badRequest({
+ body: {
+ message: 'Indices cannot be empty',
+ },
+ });
+ }
+
+ const mappings = await client.asCurrentUser.indices.getMapping({
+ index: indices,
+ });
+ return response.ok({
+ body: {
+ mappings,
+ },
+ });
+ } catch (e) {
+ logger.error('Failed to get index mappings', e);
+ if (typeof e === 'object' && e.message) {
+ return response.badRequest({
+ body: {
+ message: e.message,
+ },
+ });
+ }
+ throw e;
+ }
+ })
+ );
}
diff --git a/x-pack/plugins/search_playground/tsconfig.json b/x-pack/plugins/search_playground/tsconfig.json
index eebfd0df9a7b3..29c144ff4bac8 100644
--- a/x-pack/plugins/search_playground/tsconfig.json
+++ b/x-pack/plugins/search_playground/tsconfig.json
@@ -44,7 +44,8 @@
"@kbn/unified-doc-viewer-plugin",
"@kbn/data-views-plugin",
"@kbn/discover-utils",
- "@kbn/data-plugin"
+ "@kbn/data-plugin",
+ "@kbn/search-index-documents"
],
"exclude": [
"target/**/*",
diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss
index 08e7be248619f..7a9557fb7d8d4 100644
--- a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss
+++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss
@@ -7,9 +7,10 @@
}
.secAccessAgreementPage__text {
- @include euiYScrollWithShadows;
max-height: 400px;
padding: $euiSize $euiSizeL 0;
+
+ @include euiYScrollWithShadows;
}
.secAccessAgreementPage__footer {
diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.scss b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.scss
index 3b162dc962400..401e821aa3e76 100644
--- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.scss
+++ b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.scss
@@ -9,10 +9,10 @@
}
.secAuthenticationStatePage__logo {
+ margin-bottom: $euiSizeXL;
+
@include kibanaCircleLogo;
@include euiBottomShadowMedium;
-
- margin-bottom: $euiSizeXL;
}
.secAuthenticationStatePage__content {
diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss
index 344cde9c7825c..a8607c4048f42 100644
--- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss
+++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss
@@ -23,11 +23,11 @@
}
&:focus {
- @include euiFocusRing;
-
border-color: transparent;
border-radius: $euiBorderRadius;
+ @include euiFocusRing;
+
.secLoginCard__title {
text-decoration: underline;
}
diff --git a/x-pack/plugins/security/public/authentication/login/login_page.scss b/x-pack/plugins/security/public/authentication/login/login_page.scss
index cdfad55ee064a..8930de1425d85 100644
--- a/x-pack/plugins/security/public/authentication/login/login_page.scss
+++ b/x-pack/plugins/security/public/authentication/login/login_page.scss
@@ -9,10 +9,10 @@
}
.loginWelcome__logo {
+ margin-bottom: $euiSizeXL;
+
@include kibanaCircleLogo;
@include euiBottomShadowMedium;
-
- margin-bottom: $euiSizeXL;
}
.loginWelcome__content {
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 5da9b87a4e267..e2b85fd123f91 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -133,7 +133,7 @@ export const APP_HOST_ISOLATION_EXCEPTIONS_PATH =
export const APP_BLOCKLIST_PATH = `${APP_PATH}${BLOCKLIST_PATH}` as const;
export const APP_RESPONSE_ACTIONS_HISTORY_PATH =
`${APP_PATH}${RESPONSE_ACTIONS_HISTORY_PATH}` as const;
-export const NOTES_MANAGEMENT_PATH = `/notes_management` as const;
+export const NOTES_PATH = `${MANAGEMENT_PATH}/notes` as const;
// cloud logs to exclude from default index pattern
export const EXCLUDE_ELASTIC_CLOUD_INDICES = ['-*elastic-cloud-logs-*'];
diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_package_policy_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_package_policy_generator.ts
index 215b2ff614379..271718d8e11fd 100644
--- a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_package_policy_generator.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_package_policy_generator.ts
@@ -14,7 +14,7 @@ type PartialPackagePolicy = Partial> & {
inputs?: PackagePolicy['inputs'];
};
-type PartialEndpointPolicyData = Partial> & {
+export type PartialEndpointPolicyData = Partial> & {
inputs?: PolicyData['inputs'];
};
diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
index e0811ef8fa821..50ae6b4069770 100644
--- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
@@ -40,6 +40,7 @@ import {
import { firstNonNullValue } from './models/ecs_safety_helpers';
import type { EventOptions } from './types/generator';
import { BaseDataGenerator } from './data_generators/base_data_generator';
+import type { PartialEndpointPolicyData } from './data_generators/fleet_package_policy_generator';
import { FleetPackagePolicyGenerator } from './data_generators/fleet_package_policy_generator';
export type Event = AlertEvent | SafeEndpointEvent;
@@ -1581,8 +1582,14 @@ export class EndpointDocGenerator extends BaseDataGenerator {
/**
* Generates a Fleet `package policy` that includes the Endpoint Policy data
*/
- public generatePolicyPackagePolicy(seed: string = 'seed'): PolicyData {
- return new FleetPackagePolicyGenerator(seed).generateEndpointPackagePolicy();
+ public generatePolicyPackagePolicy({
+ seed,
+ overrides,
+ }: {
+ seed?: string;
+ overrides?: PartialEndpointPolicyData;
+ } = {}): PolicyData {
+ return new FleetPackagePolicyGenerator(seed).generateEndpointPackagePolicy(overrides);
}
/**
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts
index 8a7ab314c1e7b..5d7cc61d1d7bd 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts
@@ -7,15 +7,17 @@
import type { PolicyConfig } from '../types';
import { PolicyOperatingSystem, ProtectionModes, AntivirusRegistrationModes } from '../types';
-import { policyFactory } from './policy_config';
+import { DefaultPolicyNotificationMessage, policyFactory } from './policy_config';
import {
disableProtections,
isPolicySetToEventCollectionOnly,
ensureOnlyEventCollectionIsAllowed,
isBillablePolicy,
getPolicyProtectionsReference,
+ checkIfPopupMessagesContainCustomNotifications,
+ resetCustomNotifications,
} from './policy_config_helpers';
-import { set } from 'lodash';
+import { get, merge, set } from 'lodash';
describe('Policy Config helpers', () => {
describe('disableProtections', () => {
@@ -223,6 +225,107 @@ describe('Policy Config helpers', () => {
}
);
});
+
+ describe('checkIfPopupMessagesContainCustomNotifications', () => {
+ let policy: PolicyConfig;
+
+ beforeEach(() => {
+ policy = policyFactory();
+ });
+
+ it('returns false when all popup messages are default', () => {
+ expect(checkIfPopupMessagesContainCustomNotifications(policy)).toBe(false);
+ });
+
+ it('returns true when any popup message is custom', () => {
+ set(policy, 'windows.popup.malware.message', 'Custom message');
+ expect(checkIfPopupMessagesContainCustomNotifications(policy)).toBe(true);
+ });
+
+ it('returns false when all popup messages are empty', () => {
+ set(policy, 'windows.popup.malware.message', '');
+ set(policy, 'mac.popup.memory_protection.message', '');
+ expect(checkIfPopupMessagesContainCustomNotifications(policy)).toBe(false);
+ });
+
+ it('returns true when any popup message is not empty or default', () => {
+ set(policy, 'linux.popup.behavior_protection.message', 'Another custom message');
+ expect(checkIfPopupMessagesContainCustomNotifications(policy)).toBe(true);
+ });
+
+ it('returns false when all popup messages are default across all OS', () => {
+ set(policy, 'windows.popup.malware.message', DefaultPolicyNotificationMessage);
+ set(policy, 'mac.popup.memory_protection.message', DefaultPolicyNotificationMessage);
+ set(policy, 'linux.popup.behavior_protection.message', DefaultPolicyNotificationMessage);
+ set(policy, 'windows.popup.ransomware.message', '');
+ expect(checkIfPopupMessagesContainCustomNotifications(policy)).toBe(false);
+ });
+ });
+
+ describe('resetCustomNotifications', () => {
+ let policy: PolicyConfig;
+
+ beforeEach(() => {
+ policy = policyFactory();
+ });
+
+ it.each([
+ 'windows.popup.malware.message',
+ 'windows.popup.behavior_protection.message',
+ 'windows.popup.memory_protection.message',
+ 'windows.popup.ransomware.message',
+ 'linux.popup.malware.message',
+ 'linux.popup.behavior_protection.message',
+ 'linux.popup.memory_protection.message',
+ 'mac.popup.malware.message',
+ 'mac.popup.behavior_protection.message',
+ 'mac.popup.memory_protection.message',
+ ])('resets %s to default message', (keyPath) => {
+ set(policy, keyPath, `Custom message`);
+ const defaultNotifications = resetCustomNotifications();
+
+ const updatedPolicy = merge({}, policy, defaultNotifications);
+ expect(get(updatedPolicy, keyPath)).toBe(DefaultPolicyNotificationMessage);
+ });
+
+ it('does not change default messages', () => {
+ set(policy, 'windows.popup.malware.message', DefaultPolicyNotificationMessage);
+ const defaultNotifications = resetCustomNotifications();
+
+ const updatedPolicy = merge({}, policy, defaultNotifications);
+ expect(get(updatedPolicy, 'windows.popup.malware.message')).toBe(
+ DefaultPolicyNotificationMessage
+ );
+ });
+
+ it('resets empty messages to default messages', () => {
+ set(policy, 'windows.popup.malware.message', '');
+ const defaultNotifications = resetCustomNotifications();
+
+ const updatedPolicy = merge({}, policy, defaultNotifications);
+ expect(get(updatedPolicy, 'windows.popup.malware.message')).toBe(
+ DefaultPolicyNotificationMessage
+ );
+ });
+
+ it('resets messages for all operating systems', () => {
+ set(policy, 'windows.popup.malware.message', 'Custom message');
+ set(policy, 'mac.popup.memory_protection.message', 'Another custom message');
+ set(policy, 'linux.popup.behavior_protection.message', 'Yet another custom message');
+ const defaultNotifications = resetCustomNotifications();
+
+ const updatedPolicy = merge({}, policy, defaultNotifications);
+ expect(get(updatedPolicy, 'windows.popup.malware.message')).toBe(
+ DefaultPolicyNotificationMessage
+ );
+ expect(get(updatedPolicy, 'mac.popup.memory_protection.message')).toBe(
+ DefaultPolicyNotificationMessage
+ );
+ expect(get(updatedPolicy, 'linux.popup.behavior_protection.message')).toBe(
+ DefaultPolicyNotificationMessage
+ );
+ });
+ });
});
// This constant makes sure that if the type `PolicyConfig` is ever modified,
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts
index 3f046e6ec15de..9b3906191b698 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts
@@ -6,6 +6,7 @@
*/
import { get, set } from 'lodash';
+import { DefaultPolicyNotificationMessage } from './policy_config';
import type { PolicyConfig } from '../types';
import { PolicyOperatingSystem, ProtectionModes, AntivirusRegistrationModes } from '../types';
@@ -22,6 +23,28 @@ const allOsValues = [
PolicyOperatingSystem.windows,
];
+const getPolicyPopupReference = (): Array<{
+ keyPath: string;
+ osList: PolicyOperatingSystem[];
+}> => [
+ {
+ keyPath: 'popup.malware.message',
+ osList: [...allOsValues],
+ },
+ {
+ keyPath: 'popup.memory_protection.message',
+ osList: [...allOsValues],
+ },
+ {
+ keyPath: 'popup.behavior_protection.message',
+ osList: [...allOsValues],
+ },
+ {
+ keyPath: 'popup.ransomware.message',
+ osList: [PolicyOperatingSystem.windows],
+ },
+];
+
export const getPolicyProtectionsReference = (): PolicyProtectionReference[] => [
{
keyPath: 'malware.mode',
@@ -210,3 +233,28 @@ export function isBillablePolicy(policy: PolicyConfig) {
return !isPolicySetToEventCollectionOnly(policy).isOnlyCollectingEvents;
}
+
+export const checkIfPopupMessagesContainCustomNotifications = (policy: PolicyConfig): boolean => {
+ const popupRefs = getPolicyPopupReference();
+
+ return popupRefs.some(({ keyPath, osList }) => {
+ return osList.some((osValue) => {
+ const fullKeyPathForOs = `${osValue}.${keyPath}`;
+ const currentValue = get(policy, fullKeyPathForOs);
+ return currentValue !== '' && currentValue !== DefaultPolicyNotificationMessage;
+ });
+ });
+};
+
+export const resetCustomNotifications = (
+ customNotification = DefaultPolicyNotificationMessage
+): Partial => {
+ const popupRefs = getPolicyPopupReference();
+
+ return popupRefs.reduce((acc, { keyPath, osList }) => {
+ osList.forEach((osValue) => {
+ set(acc, `${osValue}.${keyPath}`, customNotification);
+ });
+ return acc;
+ }, {});
+};
diff --git a/x-pack/plugins/security_solution/public/app/solution_navigation/links/app_links.ts b/x-pack/plugins/security_solution/public/app/solution_navigation/links/app_links.ts
index 3107bb93de269..f6a51f1d25f4f 100644
--- a/x-pack/plugins/security_solution/public/app/solution_navigation/links/app_links.ts
+++ b/x-pack/plugins/security_solution/public/app/solution_navigation/links/app_links.ts
@@ -6,9 +6,13 @@
*/
import { SecurityPageName } from '@kbn/security-solution-navigation';
-import { cloneDeep, remove } from 'lodash';
+import { cloneDeep, remove, find } from 'lodash';
import type { AppLinkItems, LinkItem } from '../../../common/links/types';
-import { createInvestigationsLinkFromTimeline } from './sections/investigations_links';
+import {
+ createInvestigationsLinkFromNotes,
+ createInvestigationsLinkFromTimeline,
+ updateInvestigationsLinkFromNotes,
+} from './sections/investigations_links';
import { mlAppLink } from './sections/ml_links';
import { createAssetsLinkFromManage } from './sections/assets_links';
import { createSettingsLinksFromManage } from './sections/settings_links';
@@ -26,6 +30,19 @@ export const solutionAppLinksSwitcher = (appLinks: AppLinkItems): AppLinkItems =
solutionAppLinks.push(createInvestigationsLinkFromTimeline(timelineLinkItem));
}
+ // Remove note link
+ const investigationsLinkItem = find(solutionAppLinks, { id: SecurityPageName.investigations });
+ const [noteLinkItem] = remove(solutionAppLinks, { id: SecurityPageName.notes });
+ if (noteLinkItem) {
+ if (!investigationsLinkItem) {
+ solutionAppLinks.push(createInvestigationsLinkFromNotes(noteLinkItem));
+ } else {
+ solutionAppLinks.push(
+ updateInvestigationsLinkFromNotes(investigationsLinkItem, noteLinkItem)
+ );
+ }
+ }
+
// Remove manage link
const [manageLinkItem] = remove(solutionAppLinks, { id: SecurityPageName.administration });
diff --git a/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts b/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts
index 33bc73ee3ef77..3c4d059474993 100644
--- a/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts
+++ b/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts
@@ -6,6 +6,7 @@
*/
import { ExternalPageName, SecurityPageName } from '@kbn/security-solution-navigation';
+import IconFilebeatChart from './icons/filebeat_chart';
import { INVESTIGATIONS_PATH } from '../../../../../common/constants';
import { SERVER_APP_ID } from '../../../../../common';
import type { LinkItem } from '../../../../common/links/types';
@@ -21,7 +22,7 @@ const investigationsAppLink: LinkItem = {
capabilities: [`${SERVER_APP_ID}.show`],
hideTimeline: true,
skipUrlState: true,
- links: [], // timeline link are added in createInvestigationsLinkFromTimeline
+ links: [], // timeline and note links are added via the methods below
};
export const createInvestigationsLinkFromTimeline = (timelineLink: LinkItem): LinkItem => {
@@ -33,6 +34,29 @@ export const createInvestigationsLinkFromTimeline = (timelineLink: LinkItem): Li
};
};
+export const createInvestigationsLinkFromNotes = (noteLink: LinkItem): LinkItem => {
+ return {
+ ...investigationsAppLink,
+ links: [{ ...noteLink, description: i18n.NOTE_DESCRIPTION, landingIcon: IconTimelineLazy }],
+ };
+};
+
+export const updateInvestigationsLinkFromNotes = (
+ investigationsLink: LinkItem,
+ noteLink: LinkItem
+): LinkItem => {
+ const currentLinks = investigationsLink.links ?? [];
+ currentLinks.push({
+ ...noteLink,
+ description: i18n.NOTE_DESCRIPTION,
+ landingIcon: IconFilebeatChart,
+ });
+ return {
+ ...investigationsLink,
+ links: currentLinks,
+ };
+};
+
// navLinks define the navigation links for the Security Solution pages and External pages as well
export const investigationsNavLinks: SolutionNavLink[] = [
{
diff --git a/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_translations.ts b/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_translations.ts
index 55c6fe74f846d..d70717783870a 100644
--- a/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_translations.ts
+++ b/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_translations.ts
@@ -21,6 +21,13 @@ export const TIMELINE_DESCRIPTION = i18n.translate(
}
);
+export const NOTE_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.navLinks.investigations.note.title',
+ {
+ defaultMessage: 'Oversee, revise and revisit the annotations within each document and timeline',
+ }
+);
+
export const OSQUERY_TITLE = i18n.translate(
'xpack.securitySolution.navLinks.investigations.osquery.title',
{
diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts
index dcadd74245f24..97f07ee6706b9 100644
--- a/x-pack/plugins/security_solution/public/app/translations.ts
+++ b/x-pack/plugins/security_solution/public/app/translations.ts
@@ -25,7 +25,7 @@ export const ENTITY_ANALYTICS_RISK_SCORE = i18n.translate(
}
);
-export const NOTES = i18n.translate('xpack.securitySolution.navigation.notesManagement', {
+export const NOTES = i18n.translate('xpack.securitySolution.navigation.notes', {
defaultMessage: 'Notes',
});
diff --git a/x-pack/plugins/security_solution/public/app_links.ts b/x-pack/plugins/security_solution/public/app_links.ts
index 4140f6bfcd322..334209b744580 100644
--- a/x-pack/plugins/security_solution/public/app_links.ts
+++ b/x-pack/plugins/security_solution/public/app_links.ts
@@ -6,6 +6,7 @@
*/
import type { CoreStart } from '@kbn/core/public';
+import { links as notesLink } from './notes/links';
import { links as attackDiscoveryLinks } from './attack_discovery/links';
import type { AppLinkItems } from './common/links/types';
import { indicatorsLinks } from './threat_intelligence/links';
@@ -35,6 +36,7 @@ export const appLinks: AppLinkItems = Object.freeze([
rulesLinks,
gettingStartedLinks,
managementLinks,
+ notesLink,
]);
export const getFilteredLinks = async (
@@ -55,5 +57,6 @@ export const getFilteredLinks = async (
rulesLinks,
gettingStartedLinks,
managementFilteredLinks,
+ notesLink,
]);
};
diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx
index 3fa8cd2be11bc..07ff05f507029 100644
--- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx
@@ -9,7 +9,7 @@ import { isEmpty } from 'lodash/fp';
import type { TimelineType } from '../../../../common/api/timeline';
import { appendSearch } from './helpers';
-export const getTimelineTabsUrl = (tabName: TimelineType | 'notes', search?: string) =>
+export const getTimelineTabsUrl = (tabName: TimelineType, search?: string) =>
`/${tabName}${appendSearch(search)}`;
export const getTimelineUrl = (id: string, graphEventId?: string) =>
diff --git a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts
index aca8f865fce8a..82b321abdcd6e 100644
--- a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts
+++ b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts
@@ -14,6 +14,7 @@ import {
BLOCKLIST,
RESPONSE_ACTIONS_HISTORY,
PROTECTION_UPDATES,
+ NOTES,
} from '../../app/translations';
const TabNameMappedToI18nKey: Record = {
@@ -25,6 +26,7 @@ const TabNameMappedToI18nKey: Record = {
[AdministrationSubTab.blocklist]: BLOCKLIST,
[AdministrationSubTab.responseActionsHistory]: RESPONSE_ACTIONS_HISTORY,
[AdministrationSubTab.protectionUpdates]: PROTECTION_UPDATES,
+ [AdministrationSubTab.notes]: NOTES,
};
export function getTrailingBreadcrumbs(params: AdministrationRouteSpyState): ChromeBreadcrumb[] {
diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts
index 4b050266f3ac3..b319adbd0faeb 100644
--- a/x-pack/plugins/security_solution/public/management/common/constants.ts
+++ b/x-pack/plugins/security_solution/public/management/common/constants.ts
@@ -18,6 +18,7 @@ export const MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH = `${MANAGEMEN
export const MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/hostIsolationExceptions`;
export const MANAGEMENT_ROUTING_POLICY_DETAILS_BLOCKLISTS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/blocklists`;
export const MANAGEMENT_ROUTING_POLICY_DETAILS_PROTECTION_UPDATES_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/protectionUpdates`;
+export const MANAGEMENT_ROUTING_NOTES_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.notes})`;
/** @deprecated use the paths defined above instead */
export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH_OLD = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId`;
export const MANAGEMENT_ROUTING_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.trustedApps})`;
diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
index 94dddba539ce2..2408dad4f39f3 100644
--- a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
@@ -22,12 +22,16 @@ import {
EuiLoadingSpinner,
EuiLink,
EuiSkeletonText,
+ EuiCallOut,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
+import { INTEGRATIONS_PLUGIN_ID } from '@kbn/fleet-plugin/common';
+import { pagePathGetters } from '@kbn/fleet-plugin/public';
+import type { ImmutableArray, PolicyData } from '../../../common/endpoint/types';
import { useUserPrivileges } from '../../common/components/user_privileges';
import onboardingLogo from '../images/security_administration_onboarding.svg';
-import { useKibana } from '../../common/lib/kibana';
+import { useAppUrl, useKibana } from '../../common/lib/kibana';
const TEXT_ALIGN_CENTER: CSSProperties = Object.freeze({
textAlign: 'center',
@@ -103,12 +107,12 @@ const PolicyEmptyState = React.memo<{
{policyEntryPoint ? (
) : (
)}
@@ -170,107 +174,216 @@ const EndpointsEmptyState = React.memo<{
actionDisabled: boolean;
handleSelectableOnChange: (o: EuiSelectableProps['options']) => void;
selectionOptions: EuiSelectableProps['options'];
-}>(({ loading, onActionClick, actionDisabled, handleSelectableOnChange, selectionOptions }) => {
- const policySteps = useMemo(
- () => [
- {
- title: i18n.translate('xpack.securitySolution.endpoint.list.stepOneTitle', {
- defaultMessage: 'Select the integration you want to use',
- }),
- children: (
+ policyItems: ImmutableArray;
+}>(
+ ({
+ loading,
+ onActionClick,
+ actionDisabled,
+ handleSelectableOnChange,
+ selectionOptions,
+ policyItems,
+ }) => {
+ const { getAppUrl } = useAppUrl();
+ const policyItemsWithoutAgentPolicy = useMemo(
+ () => policyItems.filter((policy) => !policy.policy_ids.length),
+ [policyItems]
+ );
+
+ const policiesNotAddedToAgentPolicyCallout = useMemo(
+ () =>
+ !!policyItemsWithoutAgentPolicy.length && (
<>
-
-
-
-
-
+
- {(list) => {
- return loading ? (
-
-
-
- ) : selectionOptions.length ? (
- list
- ) : (
-
- );
- }}
-
+
+
+
+
+
+
+
+
+ {policyItemsWithoutAgentPolicy.map((policyItem) => (
+
+
+ {policyItem.name}
+
+
+ ))}
+
+
+
+
+
+ ),
+ }}
+ />
+
+
>
),
- },
- {
- title: i18n.translate('xpack.securitySolution.endpoint.list.stepTwoTitle', {
- defaultMessage: 'Enroll your agents enabled with Elastic Defend through Fleet',
- }),
- status: actionDisabled ? 'disabled' : '',
- children: (
-
-
+ [getAppUrl, policyItemsWithoutAgentPolicy]
+ );
+
+ const policySteps = useMemo(
+ () => [
+ {
+ title: i18n.translate('xpack.securitySolution.endpoint.list.stepOneTitle', {
+ defaultMessage: 'Select the integration you want to use',
+ }),
+ children: (
+ <>
-
-
-
+
-
-
-
-
- ),
- },
- ],
- [selectionOptions, handleSelectableOnChange, loading, actionDisabled, onActionClick]
- );
+ {(list) => {
+ if (loading) {
+ return (
+
+
+
+ );
+ }
- return (
-
- }
- bodyComponent={
-
- }
- />
- );
-});
+ if (!selectionOptions.length) {
+ return (
+
+
+
+ );
+ }
+
+ return list;
+ }}
+
+
+ {policiesNotAddedToAgentPolicyCallout}
+ >
+ ),
+ },
+ {
+ title: i18n.translate('xpack.securitySolution.endpoint.list.stepTwoTitle', {
+ defaultMessage: 'Enroll your agents enabled with Elastic Defend through Fleet',
+ }),
+ status: actionDisabled ? 'disabled' : '',
+ children: (
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ },
+ ],
+ [
+ selectionOptions,
+ loading,
+ handleSelectableOnChange,
+ policiesNotAddedToAgentPolicyCallout,
+ actionDisabled,
+ onActionClick,
+ ]
+ );
+
+ return (
+
+ }
+ bodyComponent={
+
+ }
+ />
+ );
+ }
+);
const ManagementEmptyState = React.memo<{
loading: boolean;
@@ -284,7 +397,11 @@ const ManagementEmptyState = React.memo<{
{loading ? (
-
+
) : (
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/components/policy_details_endpoint_complete.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/components/policy_details_endpoint_complete.cy.ts
index 98a01b3ec565d..a9444f86c07a4 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/components/policy_details_endpoint_complete.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/components/policy_details_endpoint_complete.cy.ts
@@ -49,7 +49,7 @@ describe(
login();
});
- it('should display upselling section for protection updates', () => {
+ it('should not display upselling section for protection updates', () => {
loadPage(`${APP_POLICIES_PATH}/${policyId}/protectionUpdates`);
[
'endpointPolicy-protectionUpdatesLockedCard-title',
@@ -67,5 +67,29 @@ describe(
cy.getByTestSubj(testSubj).should('exist').and('be.visible');
});
});
+
+ it(`should not display upselling section for custom notification`, () => {
+ const testData = ['malware', 'ransomware', 'memory', 'behaviour'];
+
+ loadPage(`${APP_POLICIES_PATH}/${policyId}/settings`);
+
+ testData.forEach((protection) => {
+ cy.getByTestSubj(`endpointPolicyForm-${protection}`).within(() => {
+ cy.getByTestSubj(`endpointPolicyForm-${protection}-enableDisableSwitch`).click();
+ // User should not see the locked card since the feature is available under Endpoint Complete tier
+ [
+ 'endpointPolicy-customNotificationLockedCard-title',
+ 'endpointPolicy-customNotificationLockedCard',
+ 'endpointPolicy-customNotificationLockedCard-badge',
+ ].forEach((testSubj) => {
+ cy.getByTestSubj(testSubj).should('not.exist');
+ });
+ // User should see the custom notification section
+ cy.getByTestSubj(`endpointPolicyForm-${protection}-notifyUser-customMessage`)
+ .should('exist')
+ .and('be.visible');
+ });
+ });
+ });
}
);
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/components/policy_details_endpoint_essentials.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/components/policy_details_endpoint_essentials.cy.ts
index 7a2cab9f668a3..5781fbea94880 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/components/policy_details_endpoint_essentials.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/components/policy_details_endpoint_essentials.cy.ts
@@ -60,5 +60,29 @@ describe(
});
cy.getByTestSubj('protection-updates-layout').should('not.exist');
});
+
+ it(`should display upselling section for custom notification`, () => {
+ const testData = ['malware', 'ransomware', 'memory', 'behaviour'];
+
+ loadPage(`${APP_POLICIES_PATH}/${policyId}/settings`);
+
+ testData.forEach((protection) => {
+ cy.getByTestSubj(`endpointPolicyForm-${protection}`).within(() => {
+ cy.getByTestSubj(`endpointPolicyForm-${protection}-enableDisableSwitch`).click();
+ // User should see the custom notification locked card since it is not enabled under Endpoint Essentials
+ [
+ 'endpointPolicy-customNotificationLockedCard-title',
+ 'endpointPolicy-customNotificationLockedCard',
+ 'endpointPolicy-customNotificationLockedCard-badge',
+ ].forEach((testSubj) => {
+ cy.getByTestSubj(testSubj, { timeout: 60000 }).should('exist').and('be.visible');
+ });
+ // User should not see the custom message input since it is not enabled under Endpoint Essentials
+ cy.getByTestSubj(`endpointPolicyForm-${protection}-notifyUser-customMessage`).should(
+ 'not.exist'
+ );
+ });
+ });
+ });
}
);
diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts
index 91bf4e958f6fb..7247c45c366f4 100644
--- a/x-pack/plugins/security_solution/public/management/links.ts
+++ b/x-pack/plugins/security_solution/public/management/links.ts
@@ -8,6 +8,7 @@
import type { CoreStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
+import IconFilebeatChart from '../app/solution_navigation/links/sections/icons/filebeat_chart';
import { checkArtifactHasData } from './services/exceptions_list/check_artifact_has_data';
import {
calculateEndpointAuthz,
@@ -22,6 +23,7 @@ import {
EVENT_FILTERS_PATH,
HOST_ISOLATION_EXCEPTIONS_PATH,
MANAGE_PATH,
+ NOTES_PATH,
POLICIES_PATH,
RESPONSE_ACTIONS_HISTORY_PATH,
SecurityPageName,
@@ -39,6 +41,7 @@ import {
TRUSTED_APPLICATIONS,
ENTITY_ANALYTICS_RISK_SCORE,
ASSET_CRITICALITY,
+ NOTES,
} from '../app/translations';
import { licenseService } from '../common/hooks/use_license';
import type { LinkItem } from '../common/links/types';
@@ -85,6 +88,12 @@ const categories = [
}),
linkIds: [SecurityPageName.cloudDefendPolicies],
},
+ {
+ label: i18n.translate('xpack.securitySolution.appLinks.category.investigations', {
+ defaultMessage: 'Investigations',
+ }),
+ linkIds: [SecurityPageName.notes],
+ },
];
export const links: LinkItem = {
@@ -215,6 +224,19 @@ export const links: LinkItem = {
hideTimeline: true,
},
cloudDefendLink,
+ {
+ id: SecurityPageName.notes,
+ title: NOTES,
+ description: i18n.translate('xpack.securitySolution.appLinks.notesDescription', {
+ defaultMessage:
+ 'Oversee, revise and revisit the annotations within each document and timeline.',
+ }),
+ landingIcon: IconFilebeatChart,
+ path: NOTES_PATH,
+ skipUrlState: true,
+ hideTimeline: true,
+ experimentalKey: 'securitySolutionNotesEnabled',
+ },
],
};
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
index ec27500a45e12..56b92e4692edc 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
@@ -156,10 +156,6 @@ const getAgentAndPoliciesForEndpointsList = async (
return;
}
- // We use the Agent Policy API here, instead of the Package Policy, because we can't use
- // filter by ID of the Saved Object. Agent Policy, however, keeps a reference (array) of
- // Package Ids that it uses, thus if a reference exists there, then the package policy (policy)
- // exists.
const policiesFound = (
await sendBulkGetPackagePolicies(http, policyIdsToCheck)
).items.reduce(
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index a5e09dc6d553c..adfc164e98b12 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -10,6 +10,7 @@ import * as reactTestingLibrary from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { EndpointList } from '.';
import { createUseUiSetting$Mock } from '../../../../common/lib/kibana/kibana_react.mock';
+import type { DeepPartial } from '@kbn/utility-types';
import {
mockEndpointDetailsApiResult,
@@ -57,6 +58,8 @@ import { getEndpointPrivilegesInitialStateMock } from '../../../../common/compon
import { useGetEndpointDetails } from '../../../hooks/endpoint/use_get_endpoint_details';
import { useGetAgentStatus as _useGetAgentStatus } from '../../../hooks/agents/use_get_agent_status';
import { agentStatusMocks } from '../../../../../common/endpoint/service/response_actions/mocks/agent_status.mocks';
+import { useBulkGetAgentPolicies } from '../../../services/policies/hooks';
+import type { PartialEndpointPolicyData } from '../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
const mockUserPrivileges = useUserPrivileges as jest.Mock;
// not sure why this can't be imported from '../../../../common/mock/formatted_relative';
@@ -85,6 +88,14 @@ jest.mock('../../../services/policies/ingest', () => {
jest.mock('../../../hooks/agents/use_get_agent_status');
const useGetAgentStatusMock = _useGetAgentStatus as jest.Mock;
+jest.mock('../../../services/policies/hooks', () => ({
+ ...jest.requireActual('../../../services/policies/hooks'),
+ useBulkGetAgentPolicies: jest.fn().mockReturnValue({}),
+}));
+const useBulkGetAgentPoliciesMock = useBulkGetAgentPolicies as unknown as jest.Mock<
+ DeepPartial>
+>;
+
const mockUseUiSetting$ = useUiSetting$ as jest.Mock;
const timepickerRanges = [
{
@@ -149,6 +160,7 @@ describe('when on the endpoint list page', () => {
const { act, screen, fireEvent } = reactTestingLibrary;
let render: () => ReturnType;
+ let renderResult: reactTestingLibrary.RenderResult;
let history: AppContextTestRender['history'];
let store: AppContextTestRender['store'];
let coreStart: AppContextTestRender['coreStart'];
@@ -170,7 +182,7 @@ describe('when on the endpoint list page', () => {
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
({ history, store, coreStart, middlewareSpy } = mockedContext);
- render = () => mockedContext.render( );
+ render = () => (renderResult = mockedContext.render( ));
reactTestingLibrary.act(() => {
history.push(`${MANAGEMENT_PATH}/endpoints`);
});
@@ -186,9 +198,9 @@ describe('when on the endpoint list page', () => {
endpointsResults: [],
});
- const renderResult = render();
+ render();
const timelineFlyout = renderResult.queryByTestId('timeline-bottom-bar-title-button');
- expect(timelineFlyout).toBeNull();
+ expect(timelineFlyout).not.toBeInTheDocument();
});
describe('when there are no endpoints or polices', () => {
@@ -199,47 +211,200 @@ describe('when on the endpoint list page', () => {
});
it('should show the empty state when there are no hosts or polices', async () => {
- const renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding');
});
// Initially, there are no hosts or policies, so we prompt to add policies first.
const table = await renderResult.findByTestId('emptyPolicyTable');
- expect(table).not.toBeNull();
+ expect(table).toBeInTheDocument();
});
});
describe('when there are policies, but no hosts', () => {
- let renderResult: ReturnType;
- beforeEach(async () => {
- const policyData = mockPolicyResultList({ total: 3 }).items;
+ const getOptionsTexts = async () => {
+ const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect');
+ const options = onboardingPolicySelect.querySelectorAll('[role=option]');
+
+ return [...options].map(({ textContent }) => textContent);
+ };
+
+ const setupPolicyDataMocks = (
+ partialPolicyData: PartialEndpointPolicyData[] = [
+ { name: 'Package 1', policy_ids: ['policy-1'] },
+ ]
+ ) => {
+ const policyData = partialPolicyData.map((overrides) =>
+ docGenerator.generatePolicyPackagePolicy({ overrides })
+ );
+
setEndpointListApiMockImplementation(coreStart.http, {
endpointsResults: [],
endpointPackagePolicies: policyData,
});
+ };
- renderResult = render();
- await reactTestingLibrary.act(async () => {
- await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding');
+ beforeEach(async () => {
+ useBulkGetAgentPoliciesMock.mockReturnValue({
+ data: [
+ { id: 'policy-1', name: 'Agent Policy 1' },
+ { id: 'policy-2', name: 'Agent Policy 2' },
+ { id: 'policy-3', name: 'Agent Policy 3' },
+ ],
+ isLoading: false,
});
+
+ setupPolicyDataMocks();
});
+
afterEach(() => {
jest.clearAllMocks();
});
- it('should show the no hosts empty state', async () => {
+ it('should show loading spinner while Agent Policies are loading', async () => {
+ useBulkGetAgentPoliciesMock.mockReturnValue({ isLoading: true });
+ render();
+ expect(
+ await renderResult.findByTestId('management-empty-state-loading-spinner')
+ ).toBeInTheDocument();
+ });
+
+ it('should show the no hosts empty state without loading spinner', async () => {
+ render();
+
+ expect(
+ renderResult.queryByTestId('management-empty-state-loading-spinner')
+ ).not.toBeInTheDocument();
+
const emptyHostsTable = await renderResult.findByTestId('emptyHostsTable');
- expect(emptyHostsTable).not.toBeNull();
+ expect(emptyHostsTable).toBeInTheDocument();
});
it('should display the onboarding steps', async () => {
+ render();
const onboardingSteps = await renderResult.findByTestId('onboardingSteps');
- expect(onboardingSteps).not.toBeNull();
+ expect(onboardingSteps).toBeInTheDocument();
});
- it('should show policy selection', async () => {
- const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect');
- expect(onboardingPolicySelect).not.toBeNull();
+ describe('policy selection', () => {
+ it('should show policy selection', async () => {
+ render();
+ const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect');
+ expect(onboardingPolicySelect).toBeInTheDocument();
+ });
+
+ it('should show discrete `package policy - agent policy` pairs', async () => {
+ setupPolicyDataMocks([
+ { name: 'Package 1', policy_ids: ['policy-1'] },
+ { name: 'Package 2', policy_ids: ['policy-2'] },
+ ]);
+
+ render();
+ const optionsTexts = await getOptionsTexts();
+
+ expect(optionsTexts).toStrictEqual([
+ 'Package 1 - Agent Policy 1',
+ 'Package 2 - Agent Policy 2',
+ ]);
+ });
+
+ it('should display the same package policy with multiple Agent Policies multiple times', async () => {
+ setupPolicyDataMocks([
+ { name: 'Package 1', policy_ids: ['policy-1', 'policy-2', 'policy-3'] },
+ ]);
+
+ render();
+ const optionsTexts = await getOptionsTexts();
+
+ expect(optionsTexts).toStrictEqual([
+ 'Package 1 - Agent Policy 1',
+ 'Package 1 - Agent Policy 2',
+ 'Package 1 - Agent Policy 3',
+ ]);
+ });
+
+ it('should not display a package policy without agent policy', async () => {
+ setupPolicyDataMocks([
+ { name: 'Package 1', policy_ids: [] },
+ { name: 'Package 2', policy_ids: ['policy-1'] },
+ ]);
+
+ render();
+ const optionsTexts = await getOptionsTexts();
+
+ expect(optionsTexts).toStrictEqual(['Package 2 - Agent Policy 1']);
+ });
+
+ it("should fallback to agent policy ID if it's not found", async () => {
+ setupPolicyDataMocks([{ name: 'Package 1', policy_ids: ['agent-policy-id'] }]);
+
+ render();
+ const optionsTexts = await getOptionsTexts();
+ expect(
+ renderResult.queryByTestId('noIntegrationsAddedToAgentPoliciesCallout')
+ ).not.toBeInTheDocument();
+
+ expect(optionsTexts).toStrictEqual(['Package 1 - agent-policy-id']);
+ });
+
+ it('should show callout indicating that none of the integrations are added to agent policies', async () => {
+ setupPolicyDataMocks([{ name: 'Package 1', policy_ids: [] }]);
+
+ render();
+
+ expect(
+ await renderResult.findByTestId('noIntegrationsAddedToAgentPoliciesCallout')
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('integration not added to agent policy callout', () => {
+ it('should not display callout if all integrations are added to agent policies', async () => {
+ setupPolicyDataMocks([
+ { name: 'Package 1', policy_ids: ['policy-1'] },
+ { name: 'Package 2', policy_ids: ['policy-2'] },
+ ]);
+
+ render();
+ await getOptionsTexts();
+
+ expect(
+ renderResult.queryByTestId('integrationsNotAddedToAgentPolicyCallout')
+ ).not.toBeInTheDocument();
+ });
+
+ it('should display callout if an integration is not added to an agent policy', async () => {
+ setupPolicyDataMocks([
+ { name: 'Package 1', policy_ids: ['policy-1'] },
+ { name: 'Package 2', policy_ids: [] },
+ ]);
+
+ render();
+
+ expect(
+ await renderResult.findByTestId('integrationsNotAddedToAgentPolicyCallout')
+ ).toBeInTheDocument();
+ });
+
+ it('should list all integrations which are not added to an agent policy', async () => {
+ setupPolicyDataMocks([
+ { name: 'Package 1', policy_ids: ['policy-1'] },
+ { name: 'Package 2', policy_ids: [] },
+ { name: 'Package 3', policy_ids: [] },
+ { name: 'Package 4', policy_ids: [] },
+ ]);
+
+ render();
+
+ const integrations = await renderResult.findAllByTestId(
+ 'integrationWithoutAgentPolicyListItem'
+ );
+ expect(integrations.map(({ textContent }) => textContent)).toStrictEqual([
+ 'Package 2',
+ 'Package 3',
+ 'Package 4',
+ ]);
+ });
});
});
@@ -349,7 +514,7 @@ describe('when on the endpoint list page', () => {
});
it('should display rows in the table', async () => {
- const renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
@@ -357,7 +522,7 @@ describe('when on the endpoint list page', () => {
expect(rows).toHaveLength(6);
});
it('should show total', async () => {
- const renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
@@ -365,7 +530,7 @@ describe('when on the endpoint list page', () => {
expect(total.textContent).toEqual('Showing 5 endpoints');
});
it('should agent status', async () => {
- const renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
@@ -380,7 +545,7 @@ describe('when on the endpoint list page', () => {
});
it('should display correct policy status', async () => {
- const renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
@@ -394,12 +559,12 @@ describe('when on the endpoint list page', () => {
POLICY_STATUS_TO_HEALTH_COLOR[generatedPolicyStatuses[index]]
}]`
)
- ).not.toBeNull();
+ ).toBeInTheDocument();
});
});
it('should display policy out-of-date warning when changes pending', async () => {
- const renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
@@ -412,12 +577,12 @@ describe('when on the endpoint list page', () => {
});
it('should display policy name as a link', async () => {
- const renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink-link'))[0];
- expect(firstPolicyName).not.toBeNull();
+ expect(firstPolicyName).toBeInTheDocument();
expect(firstPolicyName.getAttribute('href')).toEqual(
`${APP_PATH}${MANAGEMENT_PATH}/policy/${firstPolicyID}/settings`
);
@@ -425,7 +590,6 @@ describe('when on the endpoint list page', () => {
describe('when the user clicks the first hostname in the table', () => {
const endpointDetails: HostInfo = mockEndpointDetailsApiResult();
- let renderResult: reactTestingLibrary.RenderResult;
beforeEach(async () => {
mockUseGetEndpointDetails.mockReturnValue({
data: {
@@ -447,7 +611,7 @@ describe('when on the endpoint list page', () => {
},
},
});
- renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
@@ -459,20 +623,20 @@ describe('when on the endpoint list page', () => {
it('should show the flyout', async () => {
return renderResult.findByTestId('endpointDetailsFlyout').then((flyout) => {
- expect(flyout).not.toBeNull();
+ expect(flyout).toBeInTheDocument();
});
});
});
it('should show revision number', async () => {
- const renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
const firstPolicyRevElement = (
await renderResult.findAllByTestId('policyNameCellLink-revision')
)[0];
- expect(firstPolicyRevElement).not.toBeNull();
+ expect(firstPolicyRevElement).toBeInTheDocument();
expect(firstPolicyRevElement.textContent).toEqual(`rev. ${firstPolicyRev}`);
});
});
@@ -502,7 +666,7 @@ describe('when on the endpoint list page', () => {
});
it('should update data after some time', async () => {
- let renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
@@ -518,7 +682,7 @@ describe('when on the endpoint list page', () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
- renderResult = render();
+ render();
const updatedTotal = await renderResult.findAllByTestId('endpointListTableTotal');
expect(updatedTotal[0].textContent).toEqual('1 Host');
@@ -601,33 +765,33 @@ describe('when on the endpoint list page', () => {
});
it('should show the flyout and footer', async () => {
- const renderResult = render();
- expect(renderResult.getByTestId('endpointDetailsFlyout')).not.toBeNull();
- expect(renderResult.getByTestId('endpointDetailsFlyoutFooter')).not.toBeNull();
+ render();
+ expect(renderResult.getByTestId('endpointDetailsFlyout')).toBeInTheDocument();
+ expect(renderResult.getByTestId('endpointDetailsFlyoutFooter')).toBeInTheDocument();
});
it('should display policy name value as a link', async () => {
- const renderResult = render();
+ render();
const policyDetailsLink = await renderResult.findByTestId('policyNameCellLink-link');
- expect(policyDetailsLink).not.toBeNull();
+ expect(policyDetailsLink).toBeInTheDocument();
expect(policyDetailsLink.getAttribute('href')).toEqual(
`${APP_PATH}${MANAGEMENT_PATH}/policy/${hostInfo.metadata.Endpoint.policy.applied.id}/settings`
);
});
it('should display policy revision number', async () => {
- const renderResult = render();
+ render();
const policyDetailsRevElement = await renderResult.findByTestId(
'policyNameCellLink-revision'
);
- expect(policyDetailsRevElement).not.toBeNull();
+ expect(policyDetailsRevElement).toBeInTheDocument();
expect(policyDetailsRevElement.textContent).toEqual(
`rev. ${hostInfo.metadata.Endpoint.policy.applied.endpoint_policy_version}`
);
});
it('should update the URL when policy name link is clicked', async () => {
- const renderResult = render();
+ render();
const policyDetailsLink = await renderResult.findByTestId('policyNameCellLink-link');
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
reactTestingLibrary.act(() => {
@@ -640,7 +804,7 @@ describe('when on the endpoint list page', () => {
});
it('should update the URL when policy status link is clicked', async () => {
- const renderResult = render();
+ render();
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
reactTestingLibrary.act(() => {
@@ -654,7 +818,7 @@ describe('when on the endpoint list page', () => {
it('should display Success overall policy status', async () => {
getMockUseEndpointDetails(HostPolicyResponseActionStatus.success);
- const renderResult = render();
+ render();
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
expect(renderResult.getByTestId('policyStatusValue-success')).toBeTruthy();
expect(policyStatusBadge.textContent).toEqual('Success');
@@ -662,7 +826,7 @@ describe('when on the endpoint list page', () => {
it('should display Warning overall policy status', async () => {
getMockUseEndpointDetails(HostPolicyResponseActionStatus.warning);
- const renderResult = render();
+ render();
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
expect(policyStatusBadge.textContent).toEqual('Warning');
expect(renderResult.getByTestId('policyStatusValue-warning')).toBeTruthy();
@@ -670,7 +834,7 @@ describe('when on the endpoint list page', () => {
it('should display Failed overall policy status', async () => {
getMockUseEndpointDetails(HostPolicyResponseActionStatus.failure);
- const renderResult = render();
+ render();
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
expect(policyStatusBadge.textContent).toEqual('Failed');
expect(renderResult.getByTestId('policyStatusValue-failure')).toBeTruthy();
@@ -678,15 +842,15 @@ describe('when on the endpoint list page', () => {
it('should display Unknown overall policy status', async () => {
getMockUseEndpointDetails('' as HostPolicyResponseActionStatus);
- const renderResult = render();
+ render();
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
expect(policyStatusBadge.textContent).toEqual('Unknown');
expect(renderResult.getByTestId('policyStatusValue-')).toBeTruthy();
});
it('should show the Take Action button', async () => {
- const renderResult = render();
- expect(renderResult.getByTestId('endpointDetailsActionsButton')).not.toBeNull();
+ render();
+ expect(renderResult.getByTestId('endpointDetailsActionsButton')).toBeInTheDocument();
});
describe('Activity Log tab', () => {
@@ -705,8 +869,8 @@ describe('when on the endpoint list page', () => {
});
describe('when `canReadActionsLogManagement` is TRUE', () => {
- it('should start with the activity log tab as unselected', async () => {
- const renderResult = await render();
+ it('should start with the activity log tab as unselected', () => {
+ render();
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
const activityLogTab = renderResult.getByTestId(
'endpoint-details-flyout-tab-activity_log'
@@ -714,12 +878,14 @@ describe('when on the endpoint list page', () => {
expect(detailsTab).toHaveAttribute('aria-selected', 'true');
expect(activityLogTab).toHaveAttribute('aria-selected', 'false');
- expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).not.toBeNull();
- expect(renderResult.queryByTestId('endpointActivityLogFlyoutBody')).toBeNull();
+ expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).toBeInTheDocument();
+ expect(
+ renderResult.queryByTestId('endpointActivityLogFlyoutBody')
+ ).not.toBeInTheDocument();
});
it('should show the activity log content when selected', async () => {
- const renderResult = await render();
+ render();
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
const activityLogTab = renderResult.getByTestId(
'endpoint-details-flyout-tab-activity_log'
@@ -728,13 +894,13 @@ describe('when on the endpoint list page', () => {
await userEvent.click(activityLogTab);
expect(detailsTab).toHaveAttribute('aria-selected', 'false');
expect(activityLogTab).toHaveAttribute('aria-selected', 'true');
- expect(renderResult.getByTestId('endpointActivityLogFlyoutBody')).not.toBeNull();
- expect(renderResult.queryByTestId('endpointDetailsFlyoutBody')).toBeNull();
+ expect(renderResult.getByTestId('endpointActivityLogFlyoutBody')).toBeInTheDocument();
+ expect(renderResult.queryByTestId('endpointDetailsFlyoutBody')).not.toBeInTheDocument();
});
});
describe('when `canReadActionsLogManagement` is FALSE', () => {
- it('should not show the response actions history tab', async () => {
+ it('should not show the response actions history tab', () => {
mockUserPrivileges.mockReturnValue({
...mockInitialUserPrivilegesState(),
endpointPrivileges: {
@@ -744,15 +910,15 @@ describe('when on the endpoint list page', () => {
canAccessFleet: true,
},
});
- const renderResult = await render();
+ render();
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
const activityLogTab = renderResult.queryByTestId(
'endpoint-details-flyout-tab-activity_log'
);
expect(detailsTab).toHaveAttribute('aria-selected', 'true');
- expect(activityLogTab).toBeNull();
- expect(renderResult.findByTestId('endpointDetailsFlyoutBody')).not.toBeNull();
+ expect(activityLogTab).not.toBeInTheDocument();
+ expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).toBeInTheDocument();
});
it('should show the overview tab when force loading actions history tab via URL', async () => {
@@ -769,7 +935,7 @@ describe('when on the endpoint list page', () => {
history.push(`${MANAGEMENT_PATH}/endpoints?selected_endpoint=1&show=activity_log`);
});
- const renderResult = await render();
+ render();
await middlewareSpy.waitForAction('serverFinishedInitialization');
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
@@ -778,14 +944,13 @@ describe('when on the endpoint list page', () => {
);
expect(detailsTab).toHaveAttribute('aria-selected', 'true');
- expect(activityLogTab).toBeNull();
- expect(renderResult.findByTestId('endpointDetailsFlyoutBody')).not.toBeNull();
+ expect(activityLogTab).not.toBeInTheDocument();
+ expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).toBeInTheDocument();
});
});
});
describe('when showing host Policy Response panel', () => {
- let renderResult: ReturnType;
beforeEach(async () => {
coreStart.http.post.mockImplementation(async (requestOptions) => {
if (requestOptions.path === HOST_METADATA_LIST_ROUTE) {
@@ -793,7 +958,7 @@ describe('when on the endpoint list page', () => {
}
throw new Error(`POST to '${requestOptions.path}' does not have a mock response!`);
});
- renderResult = await render();
+ render();
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
reactTestingLibrary.act(() => {
@@ -806,14 +971,14 @@ describe('when on the endpoint list page', () => {
it('should hide the host details panel', async () => {
const endpointDetailsFlyout = renderResult.queryByTestId('endpointDetailsFlyoutBody');
- expect(endpointDetailsFlyout).toBeNull();
+ expect(endpointDetailsFlyout).not.toBeInTheDocument();
});
it('should display policy response sub-panel', async () => {
- expect(await renderResult.findByTestId('flyoutSubHeaderBackButton')).not.toBeNull();
+ expect(await renderResult.findByTestId('flyoutSubHeaderBackButton')).toBeInTheDocument();
expect(
await renderResult.findByTestId('endpointDetailsPolicyResponseFlyoutBody')
- ).not.toBeNull();
+ ).toBeInTheDocument();
});
it('should include the back to details link', async () => {
@@ -862,14 +1027,13 @@ describe('when on the endpoint list page', () => {
};
let isolateApiMock: ReturnType;
- let renderResult: ReturnType;
beforeEach(async () => {
getKibanaServicesMock.mockReturnValue(coreStart);
reactTestingLibrary.act(() => {
history.push(`${MANAGEMENT_PATH}/endpoints?selected_endpoint=1&show=isolate`);
});
- renderResult = render();
+ render();
await middlewareSpy.waitForAction('serverFinishedInitialization');
// Need to reset `http.post` and adjust it so that the mock for http host
@@ -880,7 +1044,7 @@ describe('when on the endpoint list page', () => {
});
it('should show the isolate form', () => {
- expect(renderResult.getByTestId('host_isolation_comment')).not.toBeNull();
+ expect(renderResult.getByTestId('host_isolation_comment')).toBeInTheDocument();
});
it('should take you back to details when back link below the flyout header is clicked', async () => {
@@ -922,7 +1086,7 @@ describe('when on the endpoint list page', () => {
it('should isolate endpoint host when confirm is clicked', async () => {
await confirmIsolateAndWaitForApiResponse();
- expect(renderResult.getByTestId('hostIsolateSuccessMessage')).not.toBeNull();
+ expect(renderResult.getByTestId('hostIsolateSuccessMessage')).toBeInTheDocument();
});
it('should navigate to details when the Complete button on success message is clicked', async () => {
@@ -946,7 +1110,7 @@ describe('when on the endpoint list page', () => {
});
await confirmIsolateAndWaitForApiResponse('failure');
- expect(renderResult.getByText('oh oh. something went wrong')).not.toBeNull();
+ expect(renderResult.getByText('oh oh. something went wrong')).toBeInTheDocument();
});
it('should reset isolation state and show form again', async () => {
@@ -954,7 +1118,7 @@ describe('when on the endpoint list page', () => {
// (`show` is NOT `isolate`), then the state should be reset so that the form show up again the next
// time `isolate host` is clicked
await confirmIsolateAndWaitForApiResponse();
- expect(renderResult.getByTestId('hostIsolateSuccessMessage')).not.toBeNull();
+ expect(renderResult.getByTestId('hostIsolateSuccessMessage')).toBeInTheDocument();
// Close flyout
const changeUrlAction = middlewareSpy.waitForAction('userChangedUrl');
@@ -975,7 +1139,7 @@ describe('when on the endpoint list page', () => {
});
it('should NOT show the flyout footer', () => {
- expect(renderResult.queryByTestId('endpointDetailsFlyoutFooter')).toBeNull();
+ expect(renderResult.queryByTestId('endpointDetailsFlyoutFooter')).not.toBeInTheDocument();
});
});
});
@@ -985,7 +1149,6 @@ describe('when on the endpoint list page', () => {
let hostInfo: HostInfo[];
let agentId: string;
let agentPolicyId: string;
- let renderResult: ReturnType;
let endpointActionsButton: HTMLElement;
// 2nd endpoint only has isolation capabilities
@@ -1069,7 +1232,7 @@ describe('when on the endpoint list page', () => {
history.push(`${MANAGEMENT_PATH}/endpoints`);
});
- renderResult = render();
+ render();
await middlewareSpy.waitForAction('serverReturnedEndpointList');
await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies');
@@ -1130,7 +1293,7 @@ describe('when on the endpoint list page', () => {
reactTestingLibrary.fireEvent.click(endpointActionsButton);
});
const isolateLink = screen.queryByTestId('isolateLink');
- expect(isolateLink).toBeNull();
+ expect(isolateLink).not.toBeInTheDocument();
});
it('navigates to the Security Solution Host Details page', async () => {
@@ -1179,7 +1342,7 @@ describe('when on the endpoint list page', () => {
});
render();
const banner = screen.queryByTestId('callout-endpoints-list-transform-failed');
- expect(banner).toBeNull();
+ expect(banner).not.toBeInTheDocument();
});
it('is not displayed when non-relevant transform is failing', () => {
@@ -1193,7 +1356,7 @@ describe('when on the endpoint list page', () => {
});
render();
const banner = screen.queryByTestId('callout-endpoints-list-transform-failed');
- expect(banner).toBeNull();
+ expect(banner).not.toBeInTheDocument();
});
it('is not displayed when no endpoint policy', () => {
@@ -1207,7 +1370,7 @@ describe('when on the endpoint list page', () => {
});
render();
const banner = screen.queryByTestId('callout-endpoints-list-transform-failed');
- expect(banner).toBeNull();
+ expect(banner).not.toBeInTheDocument();
});
it('is displayed when relevant transform state is failed state', async () => {
@@ -1268,12 +1431,12 @@ describe('when on the endpoint list page', () => {
canAccessFleet: true,
}),
});
- const renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding');
});
const onboardingSteps = await renderResult.findByTestId('onboardingSteps');
- expect(onboardingSteps).not.toBeNull();
+ expect(onboardingSteps).toBeInTheDocument();
});
it('user has endpoint list READ and fleet All and can view entire onboarding screen', async () => {
mockUserPrivileges.mockReturnValue({
@@ -1283,12 +1446,12 @@ describe('when on the endpoint list page', () => {
canAccessFleet: true,
}),
});
- const renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding');
});
const onboardingSteps = await renderResult.findByTestId('onboardingSteps');
- expect(onboardingSteps).not.toBeNull();
+ expect(onboardingSteps).toBeInTheDocument();
});
it('user has endpoint list ALL/READ and fleet NONE and can view a modified onboarding screen with no actions link to fleet', async () => {
mockUserPrivileges.mockReturnValue({
@@ -1298,28 +1461,26 @@ describe('when on the endpoint list page', () => {
canAccessFleet: false,
}),
});
- const renderResult = render();
+ render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding');
});
const onboardingSteps = await renderResult.findByTestId('policyOnboardingInstructions');
- expect(onboardingSteps).not.toBeNull();
+ expect(onboardingSteps).toBeInTheDocument();
const noPrivilegesPage = await renderResult.findByTestId('noFleetAccess');
- expect(noPrivilegesPage).not.toBeNull();
+ expect(noPrivilegesPage).toBeInTheDocument();
const startButton = renderResult.queryByTestId('onboardingStartButton');
- expect(startButton).toBeNull();
+ expect(startButton).not.toBeInTheDocument();
});
});
describe('endpoint list take action with RBAC controls', () => {
- let renderResult: ReturnType;
-
const renderAndClickActionsButton = async (tableRow: number = 0) => {
reactTestingLibrary.act(() => {
history.push(`${MANAGEMENT_PATH}/endpoints`);
});
- renderResult = render();
+ render();
await middlewareSpy.waitForAction('serverReturnedEndpointList');
await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies');
@@ -1408,7 +1569,7 @@ describe('when on the endpoint list page', () => {
});
await renderAndClickActionsButton();
const isolateLink = await renderResult.findByTestId('isolateLink');
- expect(isolateLink).not.toBeNull();
+ expect(isolateLink).toBeInTheDocument();
});
it('hides Isolate host option if canIsolateHost is NONE', async () => {
mockUserPrivileges.mockReturnValue({
@@ -1420,7 +1581,7 @@ describe('when on the endpoint list page', () => {
});
await renderAndClickActionsButton();
const isolateLink = screen.queryByTestId('isolateLink');
- expect(isolateLink).toBeNull();
+ expect(isolateLink).not.toBeInTheDocument();
});
it('shows unisolate host option if canUnHostIsolate is READ/ALL', async () => {
mockUserPrivileges.mockReturnValue({
@@ -1432,7 +1593,7 @@ describe('when on the endpoint list page', () => {
});
await renderAndClickActionsButton(1);
const unisolateLink = await renderResult.findByTestId('unIsolateLink');
- expect(unisolateLink).not.toBeNull();
+ expect(unisolateLink).toBeInTheDocument();
});
it('hides unisolate host option if canUnIsolateHost is NONE', async () => {
mockUserPrivileges.mockReturnValue({
@@ -1444,7 +1605,7 @@ describe('when on the endpoint list page', () => {
});
await renderAndClickActionsButton(1);
const unisolateLink = renderResult.queryByTestId('unIsolateLink');
- expect(unisolateLink).toBeNull();
+ expect(unisolateLink).not.toBeInTheDocument();
});
it('shows the Responder option when at least one rbac privilege from host isolation, process operation and file operation, is set to TRUE', async () => {
@@ -1457,7 +1618,7 @@ describe('when on the endpoint list page', () => {
});
await renderAndClickActionsButton();
const responderButton = await renderResult.findByTestId('console');
- expect(responderButton).not.toBeNull();
+ expect(responderButton).toBeInTheDocument();
});
it('hides the Responder option when host isolation, process operation and file operations are ALL set to NONE', async () => {
@@ -1470,13 +1631,13 @@ describe('when on the endpoint list page', () => {
});
await renderAndClickActionsButton();
const responderButton = renderResult.queryByTestId('console');
- expect(responderButton).toBeNull();
+ expect(responderButton).not.toBeInTheDocument();
});
it('always shows the Host details link', async () => {
mockUserPrivileges.mockReturnValue(getUserPrivilegesMockDefaultValue());
await renderAndClickActionsButton();
const hostLink = await renderResult.findByTestId('hostLink');
- expect(hostLink).not.toBeNull();
+ expect(hostLink).toBeInTheDocument();
});
it('shows Agent Policy, View Agent Details and Reassign Policy Links when canReadFleetAgents,canWriteFleetAgents,canReadFleetAgentPolicies RBAC control is enabled', async () => {
mockUserPrivileges.mockReturnValue({
@@ -1493,9 +1654,9 @@ describe('when on the endpoint list page', () => {
const agentPolicyLink = await renderResult.findByTestId('agentPolicyLink');
const agentDetailsLink = await renderResult.findByTestId('agentDetailsLink');
const agentPolicyReassignLink = await renderResult.findByTestId('agentPolicyReassignLink');
- expect(agentPolicyLink).not.toBeNull();
- expect(agentDetailsLink).not.toBeNull();
- expect(agentPolicyReassignLink).not.toBeNull();
+ expect(agentPolicyLink).toBeInTheDocument();
+ expect(agentDetailsLink).toBeInTheDocument();
+ expect(agentPolicyReassignLink).toBeInTheDocument();
});
it('hides Agent Policy, View Agent Details and Reassign Policy Links when canAccessFleet RBAC control is NOT enabled', async () => {
mockUserPrivileges.mockReturnValue({
@@ -1509,9 +1670,9 @@ describe('when on the endpoint list page', () => {
const agentPolicyLink = renderResult.queryByTestId('agentPolicyLink');
const agentDetailsLink = renderResult.queryByTestId('agentDetailsLink');
const agentPolicyReassignLink = renderResult.queryByTestId('agentPolicyReassignLink');
- expect(agentPolicyLink).toBeNull();
- expect(agentDetailsLink).toBeNull();
- expect(agentPolicyReassignLink).toBeNull();
+ expect(agentPolicyLink).not.toBeInTheDocument();
+ expect(agentDetailsLink).not.toBeInTheDocument();
+ expect(agentPolicyReassignLink).not.toBeInTheDocument();
});
});
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
index 2ed2b20ab78a7..162d05f54ec21 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
@@ -40,7 +40,7 @@ import { EndpointListNavLink } from './components/endpoint_list_nav_link';
import { AgentStatus } from '../../../../common/components/endpoint/agents/agent_status';
import { EndpointDetailsFlyout } from './details';
import * as selectors from '../store/selectors';
-import { nonExistingPolicies } from '../store/selectors';
+import type { nonExistingPolicies } from '../store/selectors';
import { useEndpointSelector } from './hooks';
import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants';
import type { CreateStructuredSelector } from '../../../../common/store';
@@ -69,6 +69,7 @@ import { APP_UI_ID } from '../../../../../common/constants';
import { ManagementEmptyStateWrapper } from '../../../components/management_empty_state_wrapper';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { BackToPolicyListButton } from './components/back_to_policy_list_button';
+import { useBulkGetAgentPolicies } from '../../../services/policies/hooks';
const MAX_PAGINATED_ITEM = 9999;
@@ -338,8 +339,8 @@ export const EndpointList = () => {
patternsError,
metadataTransformStats,
isInitialized,
+ nonExistingPolicies: missingPolicies,
} = useEndpointSelector(selector);
- const missingPolicies = useEndpointSelector(nonExistingPolicies);
const {
canReadEndpointList,
canAccessFleet,
@@ -353,24 +354,22 @@ export const EndpointList = () => {
// cap ability to page at 10k records. (max_result_window)
const maxPageCount = totalItemCount > MAX_PAGINATED_ITEM ? MAX_PAGINATED_ITEM : totalItemCount;
- const hasPolicyData = useMemo(() => policyItems && policyItems.length > 0, [policyItems]);
- const hasListData = useMemo(() => listData && listData.length > 0, [listData]);
+ const hasPolicyData = policyItems && policyItems.length > 0;
+ const hasListData = listData && listData.length > 0;
const refreshStyle = useMemo(() => {
return { display: endpointsExist ? 'flex' : 'none', maxWidth: 200 };
}, [endpointsExist]);
- const refreshIsPaused = useMemo(() => {
- return !endpointsExist ? false : hasSelectedEndpoint ? true : !isAutoRefreshEnabled;
- }, [endpointsExist, hasSelectedEndpoint, isAutoRefreshEnabled]);
+ const refreshIsPaused = !endpointsExist
+ ? false
+ : hasSelectedEndpoint
+ ? true
+ : !isAutoRefreshEnabled;
- const refreshInterval = useMemo(() => {
- return !endpointsExist ? DEFAULT_POLL_INTERVAL : autoRefreshInterval;
- }, [endpointsExist, autoRefreshInterval]);
+ const refreshInterval = !endpointsExist ? DEFAULT_POLL_INTERVAL : autoRefreshInterval;
- const shouldShowKQLBar = useMemo(() => {
- return endpointsExist && !patternsError;
- }, [endpointsExist, patternsError]);
+ const shouldShowKQLBar = endpointsExist && !patternsError;
const paginationSetup = useMemo(() => {
return {
@@ -465,6 +464,57 @@ export const EndpointList = () => {
[dispatch]
);
+ const stateToDisplay:
+ | 'loading'
+ | 'policyEmptyState'
+ | 'policyEmptyStateWithoutFleetAccess'
+ | 'hostsEmptyState'
+ | 'endpointTable'
+ | 'listError' = useMemo(() => {
+ if (!isInitialized) {
+ return 'loading';
+ } else if (listError) {
+ return 'listError';
+ } else if (endpointsExist) {
+ return 'endpointTable';
+ } else if (canReadEndpointList && !canAccessFleet) {
+ return 'policyEmptyStateWithoutFleetAccess';
+ } else if (!policyItemsLoading && hasPolicyData) {
+ return 'hostsEmptyState';
+ } else {
+ return 'policyEmptyState';
+ }
+ }, [
+ canAccessFleet,
+ canReadEndpointList,
+ endpointsExist,
+ hasPolicyData,
+ isInitialized,
+ listError,
+ policyItemsLoading,
+ ]);
+
+ const referencedAgentPolicyIds: string[] = useMemo(
+ // Agent Policy IDs should be unique as one Agent Policy can have only one Defend integration
+ () => policyItems.flatMap((item) => item.policy_ids),
+ [policyItems]
+ );
+
+ const { data: referencedAgentPolicies, isLoading: isAgentPolicesLoading } =
+ useBulkGetAgentPolicies({
+ isEnabled: stateToDisplay === 'hostsEmptyState',
+ policyIds: referencedAgentPolicyIds,
+ });
+
+ const agentPolicyNameMap = useMemo(
+ () =>
+ referencedAgentPolicies?.reduce>((acc, policy) => {
+ acc[policy.id] = policy.name;
+ return acc;
+ }, {}) ?? {},
+ [referencedAgentPolicies]
+ );
+
// Used for an auto-refresh super date picker version without any date/time selection
const onTimeChange = useCallback(() => {}, []);
@@ -526,86 +576,92 @@ export const EndpointList = () => {
);
const mutableListData = useMemo(() => [...listData], [listData]);
+
const renderTableOrEmptyState = useMemo(() => {
- if (!isInitialized) {
- return (
-
- }
- title={
-
- {i18n.translate('xpack.securitySolution.endpoint.list.loadingEndpointManagement', {
- defaultMessage: 'Loading Endpoint Management',
- })}
-
- }
+ switch (stateToDisplay) {
+ case 'loading':
+ return (
+
+ }
+ title={
+
+ {i18n.translate(
+ 'xpack.securitySolution.endpoint.list.loadingEndpointManagement',
+ {
+ defaultMessage: 'Loading Endpoint Management',
+ }
+ )}
+
+ }
+ />
+
+ );
+ case 'listError':
+ return (
+
+ {listError?.error}}
+ body={{listError?.message}
}
+ />
+
+ );
+ case 'endpointTable':
+ return (
+
-
- );
- } else if (listError) {
- return (
-
- {listError.error}}
- body={{listError.message}
}
+ );
+ case 'policyEmptyStateWithoutFleetAccess':
+ return (
+
+
+
+ );
+ case 'hostsEmptyState':
+ const selectionOptions: EuiSelectableProps['options'] = policyItems.flatMap((policy) =>
+ // displaying Package Policy - Agent Policy pairs
+ policy.policy_ids.map((agentPolicyId) => ({
+ key: agentPolicyId,
+ label: `${policy.name} - ${agentPolicyNameMap[agentPolicyId] || agentPolicyId}`,
+ checked: selectedPolicyId === agentPolicyId ? 'on' : undefined,
+ }))
+ );
+
+ return (
+
-
- );
- } else if (endpointsExist) {
- return (
-
- );
- } else if (canReadEndpointList && !canAccessFleet) {
- return (
-
-
-
- );
- } else if (!policyItemsLoading && hasPolicyData) {
- const selectionOptions: EuiSelectableProps['options'] = policyItems
- .filter((item) => item.policy_id)
- .map((item) => {
- return {
- key: item.policy_id as string,
- label: item.name,
- checked: selectedPolicyId === item.policy_id ? 'on' : undefined,
- };
- });
- return (
-
- );
- } else {
- return (
-
-
-
- );
+ );
+ case 'policyEmptyState':
+ default:
+ return (
+
+
+
+ );
}
}, [
- isInitialized,
+ stateToDisplay,
listError,
- endpointsExist,
- canReadEndpointList,
- canAccessFleet,
policyItemsLoading,
- hasPolicyData,
mutableListData,
columns,
paginationSetup,
@@ -615,6 +671,8 @@ export const EndpointList = () => {
sorting,
endpointPrivilegesLoading,
policyItems,
+ agentPolicyNameMap,
+ isAgentPolicesLoading,
handleDeployEndpointsClick,
selectedPolicyId,
handleSelectableOnChange,
diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx
index 144ae26815f18..dc8314acc276c 100644
--- a/x-pack/plugins/security_solution/public/management/pages/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx
@@ -11,6 +11,8 @@ import { Routes, Route } from '@kbn/shared-ux-router';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
+import { NotesContainer } from './notes';
import { ManagementEmptyStateWrapper } from '../components/management_empty_state_wrapper';
import {
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
@@ -20,6 +22,7 @@ import {
MANAGEMENT_ROUTING_TRUSTED_APPS_PATH,
MANAGEMENT_ROUTING_BLOCKLIST_PATH,
MANAGEMENT_ROUTING_RESPONSE_ACTIONS_HISTORY_PATH,
+ MANAGEMENT_ROUTING_NOTES_PATH,
} from '../common/constants';
import { NotFoundPage } from '../../app/404';
import { EndpointsContainer } from './endpoint_hosts';
@@ -77,7 +80,18 @@ const ResponseActionsTelemetry = () => (
);
+const NotesTelemetry = () => (
+
+
+
+
+);
+
export const ManagementContainer = memo(() => {
+ const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled(
+ 'securitySolutionNotesEnabled'
+ );
+
const {
loading,
canReadPolicyManagement,
@@ -148,6 +162,10 @@ export const ManagementContainer = memo(() => {
hasPrivilege={canReadActionsLogManagement}
/>
+ {securitySolutionNotesEnabled && (
+
+ )}
+
{canReadEndpointList && (
diff --git a/x-pack/plugins/security_solution/public/management/pages/notes/index.tsx b/x-pack/plugins/security_solution/public/management/pages/notes/index.tsx
new file mode 100644
index 0000000000000..5c57509432a93
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/notes/index.tsx
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Routes, Route } from '@kbn/shared-ux-router';
+import React from 'react';
+import { NoteManagementPage } from '../../../notes';
+import { NotFoundPage } from '../../../app/404';
+import { MANAGEMENT_ROUTING_NOTES_PATH } from '../../common/constants';
+
+export const NotesContainer = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx
index 9181e643f393a..62bfb9f665745 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx
@@ -28,6 +28,7 @@ import type { PolicyFormComponentCommonProps } from '../types';
import type { ImmutableArray, UIPolicyConfig } from '../../../../../../../common/endpoint/types';
import { ProtectionModes } from '../../../../../../../common/endpoint/types';
import type { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types';
+import { useGetCustomNotificationUnavailableComponent } from '../hooks/use_get_custom_notification_unavailable_component';
export const NOTIFY_USER_SECTION_TITLE = i18n.translate(
'xpack.securitySolution.endpoint.policyDetailsConfig.userNotification',
@@ -71,6 +72,7 @@ export const NotifyUserOption = React.memo(
}: NotifyUserOptionProps) => {
const isPlatinumPlus = useLicense().isPlatinumPlus();
const getTestId = useTestIdGenerator(dataTestSubj);
+ const CustomNotificationUpsellingComponent = useGetCustomNotificationUnavailableComponent();
const isEditMode = mode === 'edit';
const selected = policy.windows[protection].mode;
@@ -150,6 +152,93 @@ export const NotifyUserOption = React.memo(
[protection]
);
+ const customNotificationComponent = useMemo(() => {
+ if (!userNotificationSelected) {
+ return null;
+ }
+
+ if (CustomNotificationUpsellingComponent) {
+ return ;
+ }
+
+ if (!isEditMode) {
+ return (
+ <>
+
+
+ {NOTIFICATION_MESSAGE_LABEL}
+
+
+ <>{userNotificationMessage || getEmptyValue()}>
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+ {CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL}
+
+
+
+
+
+
+
+ >
+ }
+ />
+
+
+
+
+ >
+ );
+ }, [
+ CustomNotificationUpsellingComponent,
+ getTestId,
+ handleCustomUserNotification,
+ isEditMode,
+ protection,
+ tooltipBracketText,
+ tooltipProtectionText,
+ userNotificationMessage,
+ userNotificationSelected,
+ ]);
+
if (!isPlatinumPlus) {
return null;
}
@@ -177,69 +266,7 @@ export const NotifyUserOption = React.memo(
label={NOTIFY_USER_CHECKBOX_LABEL}
/>
- {userNotificationSelected &&
- (isEditMode ? (
- <>
-
-
-
-
- {CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL}
-
-
-
-
-
-
-
- >
- }
- />
-
-
-
-
- >
- ) : (
- <>
-
-
- {NOTIFICATION_MESSAGE_LABEL}
-
-
- <>{userNotificationMessage || getEmptyValue()}>
- >
- ))}
+ {customNotificationComponent}
);
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/hooks/use_get_custom_notification_unavailable_component.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/hooks/use_get_custom_notification_unavailable_component.tsx
new file mode 100644
index 0000000000000..4f7593fb2df8c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/hooks/use_get_custom_notification_unavailable_component.tsx
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type React from 'react';
+import { useUpsellingComponent } from '../../../../../../common/hooks/use_upselling';
+
+export const useGetCustomNotificationUnavailableComponent = (): React.ComponentType | null => {
+ return useUpsellingComponent('endpoint_custom_notification');
+};
diff --git a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts
index 34ccf5677d144..0e823c985c696 100644
--- a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts
+++ b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts
@@ -7,13 +7,14 @@
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import type { IHttpFetchError } from '@kbn/core-http-browser';
-import type { GetInfoResponse } from '@kbn/fleet-plugin/common';
+import type { BulkGetAgentPoliciesResponse } from '@kbn/fleet-plugin/common';
+import { type GetInfoResponse } from '@kbn/fleet-plugin/common';
import { firstValueFrom } from 'rxjs';
import type { IKibanaSearchResponse } from '@kbn/search-types';
import { ENDPOINT_PACKAGE_POLICIES_STATS_STRATEGY } from '../../../../common/endpoint/constants';
import { useHttp, useKibana } from '../../../common/lib/kibana';
import { MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../common/constants';
-import { sendGetEndpointSecurityPackage } from './ingest';
+import { sendBulkGetAgentPolicies, sendGetEndpointSecurityPackage } from './ingest';
import type { GetPolicyListResponse } from '../../pages/policy/types';
import { sendGetEndpointSpecificPackagePolicies } from './policies';
import type { ServerApiError } from '../../../common/types';
@@ -83,3 +84,23 @@ export function useGetEndpointSecurityPackage({
customQueryOptions
);
}
+
+export function useBulkGetAgentPolicies({
+ isEnabled,
+ policyIds,
+}: {
+ isEnabled: boolean;
+ policyIds: string[];
+}): QueryObserverResult {
+ const http = useHttp();
+
+ return useQuery(
+ ['agentPolicies', policyIds],
+
+ async () => {
+ return (await sendBulkGetAgentPolicies({ http, requestBody: { ids: policyIds } }))?.items;
+ },
+
+ { enabled: isEnabled, refetchOnWindowFocus: false, retry: 1 }
+ );
+}
diff --git a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts
index c125464bffdb9..2437e3d267a11 100644
--- a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts
+++ b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts
@@ -10,9 +10,12 @@ import type {
GetAgentStatusResponse,
GetPackagePoliciesResponse,
GetInfoResponse,
+ BulkGetAgentPoliciesResponse,
} from '@kbn/fleet-plugin/common';
-import { epmRouteService, API_VERSIONS } from '@kbn/fleet-plugin/common';
+import { epmRouteService, API_VERSIONS, agentPolicyRouteService } from '@kbn/fleet-plugin/common';
+import type { BulkGetAgentPoliciesRequestSchema } from '@kbn/fleet-plugin/server/types';
+import type { TypeOf } from '@kbn/config-schema';
import type { NewPolicyData } from '../../../../common/endpoint/types';
import type { GetPolicyResponse, UpdatePolicyResponse } from '../../pages/policy/types';
@@ -120,3 +123,15 @@ export const sendGetEndpointSecurityPackage = async (
}
return endpointPackageInfo;
};
+
+export const sendBulkGetAgentPolicies = async ({
+ http,
+ requestBody,
+}: {
+ http: HttpStart;
+ requestBody: TypeOf;
+}): Promise =>
+ http.post(agentPolicyRouteService.getBulkGetPath(), {
+ version: API_VERSIONS.public.v1,
+ body: JSON.stringify(requestBody),
+ });
diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts
index 94f671ac09e2f..ef0e3e56c7285 100644
--- a/x-pack/plugins/security_solution/public/management/types.ts
+++ b/x-pack/plugins/security_solution/public/management/types.ts
@@ -33,6 +33,7 @@ export enum AdministrationSubTab {
blocklist = 'blocklist',
responseActionsHistory = 'response_actions_history',
protectionUpdates = 'protection_updates',
+ notes = 'notes',
}
/**
diff --git a/x-pack/plugins/security_solution/public/notes/components/translations.ts b/x-pack/plugins/security_solution/public/notes/components/translations.ts
index f2846d6daab62..8d7a5b4262815 100644
--- a/x-pack/plugins/security_solution/public/notes/components/translations.ts
+++ b/x-pack/plugins/security_solution/public/notes/components/translations.ts
@@ -14,56 +14,10 @@ export const BATCH_ACTIONS = i18n.translate(
}
);
-export const CREATED_COLUMN = i18n.translate(
- 'xpack.securitySolution.notes.management.createdColumnTitle',
- {
- defaultMessage: 'Created',
- }
-);
-
-export const CREATED_BY_COLUMN = i18n.translate(
- 'xpack.securitySolution.notes.management.createdByColumnTitle',
- {
- defaultMessage: 'Created by',
- }
-);
-
-export const EVENT_ID_COLUMN = i18n.translate(
- 'xpack.securitySolution.notes.management.eventIdColumnTitle',
- {
- defaultMessage: 'View Document',
- }
-);
-
-export const TIMELINE_ID_COLUMN = i18n.translate(
- 'xpack.securitySolution.notes.management.timelineColumnTitle',
- {
- defaultMessage: 'Timeline',
- }
-);
-
-export const NOTE_CONTENT_COLUMN = i18n.translate(
- 'xpack.securitySolution.notes.management.noteContentColumnTitle',
- {
- defaultMessage: 'Note content',
- }
-);
-
export const DELETE = i18n.translate('xpack.securitySolution.notes.management.deleteAction', {
defaultMessage: 'Delete',
});
-export const DELETE_SINGLE_NOTE_DESCRIPTION = i18n.translate(
- 'xpack.securitySolution.notes.management.deleteDescription',
- {
- defaultMessage: 'Delete this note',
- }
-);
-
-export const TABLE_ERROR = i18n.translate('xpack.securitySolution.notes.management.tableError', {
- defaultMessage: 'Unable to load notes',
-});
-
export const DELETE_NOTES_MODAL_TITLE = i18n.translate(
'xpack.securitySolution.notes.management.deleteNotesModalTitle',
{
@@ -96,13 +50,6 @@ export const REFRESH = i18n.translate('xpack.securitySolution.notes.management.r
defaultMessage: 'Refresh',
});
-export const OPEN_TIMELINE = i18n.translate(
- 'xpack.securitySolution.notes.management.openTimeline',
- {
- defaultMessage: 'Open timeline',
- }
-);
-
export const VIEW_EVENT_IN_TIMELINE = i18n.translate(
'xpack.securitySolution.notes.management.viewEventInTimeline',
{
diff --git a/x-pack/plugins/security_solution/public/notes/links.ts b/x-pack/plugins/security_solution/public/notes/links.ts
new file mode 100644
index 0000000000000..ef6c691b6246a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/notes/links.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { NOTES_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants';
+import { NOTES } from '../app/translations';
+import type { LinkItem } from '../common/links/types';
+
+export const links: LinkItem = {
+ id: SecurityPageName.notes,
+ title: NOTES,
+ path: NOTES_PATH,
+ capabilities: [`${SERVER_APP_ID}.show`],
+ globalSearchKeywords: [
+ i18n.translate('xpack.securitySolution.appLinks.notes', {
+ defaultMessage: 'Notes',
+ }),
+ ],
+ links: [],
+ experimentalKey: 'securitySolutionNotesEnabled',
+};
diff --git a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx
index 574501b7d03c0..ddfed3fbb6287 100644
--- a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx
+++ b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx
@@ -7,8 +7,11 @@
import React, { useCallback, useMemo, useEffect } from 'react';
import type { DefaultItemAction, EuiBasicTableColumn } from '@elastic/eui';
-import { EuiBasicTable, EuiEmptyPrompt, EuiLink } from '@elastic/eui';
+import { EuiBasicTable, EuiEmptyPrompt, EuiLink, EuiSpacer } from '@elastic/eui';
import { useDispatch, useSelector } from 'react-redux';
+import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
+import { useQueryTimelineById } from '../../timelines/components/open_timeline/helpers';
+import { Title } from '../../common/components/header_page/title';
// TODO unify this type from the api with the one in public/common/lib/note
import type { Note } from '../../../common/api/timeline';
import { FormattedRelativePreferenceDate } from '../../common/components/formatted_date';
@@ -32,12 +35,11 @@ import type { NotesState } from '..';
import { SearchRow } from '../components/search_row';
import { NotesUtilityBar } from '../components/utility_bar';
import { DeleteConfirmModal } from '../components/delete_confirm_modal';
-import * as i18n from '../components/translations';
-import type { OpenTimelineProps } from '../../timelines/components/open_timeline/types';
+import * as i18n from './translations';
import { OpenEventInTimeline } from '../components/open_event_in_timeline';
const columns: (
- onOpenTimeline: OpenTimelineProps['onOpenTimeline']
+ onOpenTimeline: (timelineId: string) => void
) => Array> = (onOpenTimeline) => {
return [
{
@@ -61,9 +63,7 @@ const columns: (
name: i18n.TIMELINE_ID_COLUMN,
render: (timelineId: Note['timelineId']) =>
timelineId ? (
- onOpenTimeline({ timelineId, duplicate: false })}>
- {i18n.OPEN_TIMELINE}
-
+ onOpenTimeline(timelineId)}>{i18n.OPEN_TIMELINE}
) : null,
},
{
@@ -80,11 +80,7 @@ const pageSizeOptions = [10, 25, 50, 100];
* This component uses the same slices of state as the notes functionality of the rest of the Security Solution applicaiton.
* Therefore, changes made in this page (like fetching or deleting notes) will have an impact everywhere.
*/
-export const NoteManagementPage = ({
- onOpenTimeline,
-}: {
- onOpenTimeline: OpenTimelineProps['onOpenTimeline'];
-}) => {
+export const NoteManagementPage = () => {
const dispatch = useDispatch();
const notes = useSelector(selectAllNotes);
const pagination = useSelector(selectNotesPagination);
@@ -152,6 +148,19 @@ export const NoteManagementPage = ({
return item.noteId;
}, []);
+ const unifiedComponentsInTimelineDisabled = useIsExperimentalFeatureEnabled(
+ 'unifiedComponentsInTimelineDisabled'
+ );
+ const queryTimelineById = useQueryTimelineById();
+ const openTimeline = useCallback(
+ (timelineId: string) =>
+ queryTimelineById({
+ timelineId,
+ unifiedComponentsInTimelineDisabled,
+ }),
+ [queryTimelineById, unifiedComponentsInTimelineDisabled]
+ );
+
const columnWithActions = useMemo(() => {
const actions: Array> = [
{
@@ -164,13 +173,13 @@ export const NoteManagementPage = ({
},
];
return [
- ...columns(onOpenTimeline),
+ ...columns(openTimeline),
{
name: 'actions',
actions,
},
];
- }, [selectRowForDeletion, onOpenTimeline]);
+ }, [selectRowForDeletion, openTimeline]);
const currentPagination = useMemo(() => {
return {
@@ -207,6 +216,8 @@ export const NoteManagementPage = ({
return (
<>
+
+
(
+
+
+
+
+
+
+);
+
+const NotesManagementContainer: React.FC = React.memo(() => {
+ return (
+
+
+
+
+ );
+});
+NotesManagementContainer.displayName = 'NotesManagementContainer';
+
+export const routes = [
+ {
+ path: NOTES_PATH,
+ component: NotesManagementContainer,
+ },
+];
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx
index ea86d5eaa54fb..cdb61ecf61f6e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx
@@ -70,7 +70,7 @@ interface OwnProps {
export type OpenTimelineOwnProps = OwnProps &
Pick<
OpenTimelineProps,
- 'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle' | 'tabName'
+ 'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle'
>;
/** Returns a collection of selected timeline ids */
@@ -131,7 +131,6 @@ export const StatefulOpenTimelineComponent = React.memo(
importDataModalToggle,
onOpenTimeline,
setImportDataModalToggle,
- tabName,
title,
}) => {
const dispatch = useDispatch();
@@ -423,7 +422,6 @@ export const StatefulOpenTimelineComponent = React.memo(
selectedItems={selectedItems}
sortDirection={sortDirection}
sortField={sortField}
- tabName={tabName}
templateTimelineFilter={templateTimelineFilter}
timelineType={timelineType}
timelineStatus={timelineStatus}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
index 524d3bee9640a..5a1a9155bb5c1 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
@@ -30,7 +30,6 @@ import { TimelinesTable } from './timelines_table';
import * as i18n from './translations';
import { OPEN_TIMELINE_CLASS_NAME } from './helpers';
import type { OpenTimelineProps, ActionTimelineToShow, OpenTimelineResult } from './types';
-import { NoteManagementPage } from '../../../notes';
const QueryText = styled.span`
white-space: normal;
@@ -64,7 +63,6 @@ export const OpenTimeline = React.memo(
sortDirection,
setImportDataModalToggle,
sortField,
- tabName,
timelineType = TimelineTypeEnum.default,
timelineStatus,
timelineFilter,
@@ -228,92 +226,86 @@ export const OpenTimeline = React.memo(
{!!timelineFilter && timelineFilter}
- {tabName !== 'notes' ? (
- <>
-
- {SearchRowContent}
-
+ <>
+
+ {SearchRowContent}
+
-
-
-
-
- <>
- {i18n.SHOWING}{' '}
- {timelineType === TimelineTypeEnum.template ? nTemplates : nTimelines}
- >
-
-
-
- {timelineStatus !== TimelineStatusEnum.immutable && (
- <>
-
- {timelineType === TimelineTypeEnum.template
- ? i18n.SELECTED_TEMPLATES((selectedItems || []).length)
- : i18n.SELECTED_TIMELINES((selectedItems || []).length)}
-
-
-
- {i18n.BATCH_ACTIONS}
-
-
- >
- )}
-
- {i18n.REFRESH}
-
-
-
-
+
+
+
+
+ <>
+ {i18n.SHOWING}{' '}
+ {timelineType === TimelineTypeEnum.template ? nTemplates : nTimelines}
+ >
+
+
+
+ {timelineStatus !== TimelineStatusEnum.immutable && (
+ <>
+
+ {timelineType === TimelineTypeEnum.template
+ ? i18n.SELECTED_TEMPLATES((selectedItems || []).length)
+ : i18n.SELECTED_TIMELINES((selectedItems || []).length)}
+
+
+ {i18n.BATCH_ACTIONS}
+
+ >
+ )}
+
+ {i18n.REFRESH}
+
+
+
+
-
- >
- ) : (
-
- )}
+
+ >
>
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
index 14ddedf5b9688..d750ec08c24b4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
@@ -206,7 +206,6 @@ export interface OpenTimelineProps {
totalSearchResultsCount: number;
/** Hide action on timeline if needed it */
hideActions?: ActionTimelineToShow[];
- tabName?: string;
}
export interface ResolveTimelineConfig {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
index 45dd7d9e95bbf..e6ad5ad0e1f11 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
@@ -8,7 +8,6 @@
import React, { useState, useCallback, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui';
-
import { noop } from 'lodash/fp';
import { type TimelineType, TimelineTypeEnum } from '../../../../common/api/timeline';
import { SecurityPageName } from '../../../app/types';
@@ -17,7 +16,7 @@ import * as i18n from './translations';
import type { TimelineTab } from './types';
import { TimelineTabsStyle } from './types';
import { useKibana } from '../../../common/lib/kibana';
-import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
+
export interface UseTimelineTypesArgs {
defaultTimelineCount?: number | null;
templateTimelineCount?: number | null;
@@ -42,8 +41,6 @@ export const useTimelineTypes = ({
: TimelineTypeEnum.default
);
- const notesEnabled = useIsExperimentalFeatureEnabled('securitySolutionNotesEnabled');
-
const timelineUrl = useMemo(() => {
return formatUrl(getTimelineTabsUrl(TimelineTypeEnum.default, urlSearch));
}, [formatUrl, urlSearch]);
@@ -51,10 +48,6 @@ export const useTimelineTypes = ({
return formatUrl(getTimelineTabsUrl(TimelineTypeEnum.template, urlSearch));
}, [formatUrl, urlSearch]);
- const notesUrl = useMemo(() => {
- return formatUrl(getTimelineTabsUrl('notes', urlSearch));
- }, [formatUrl, urlSearch]);
-
const goToTimeline = useCallback(
(ev: React.SyntheticEvent) => {
ev.preventDefault();
@@ -71,14 +64,6 @@ export const useTimelineTypes = ({
[navigateToUrl, templateUrl]
);
- const goToNotes = useCallback(
- (ev: React.SyntheticEvent) => {
- ev.preventDefault();
- navigateToUrl(notesUrl);
- },
- [navigateToUrl, notesUrl]
- );
-
const getFilterOrTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = useCallback(
(timelineTabsStyle: TimelineTabsStyle) => [
{
@@ -132,17 +117,6 @@ export const useTimelineTypes = ({
{tab.name}
))}
- {notesEnabled && (
-
- {'Notes'}
-
- )}
>
diff --git a/x-pack/plugins/security_solution/public/timelines/links.ts b/x-pack/plugins/security_solution/public/timelines/links.ts
index 169ef6da01910..9315417d97646 100644
--- a/x-pack/plugins/security_solution/public/timelines/links.ts
+++ b/x-pack/plugins/security_solution/public/timelines/links.ts
@@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { SecurityPageName, SERVER_APP_ID, TIMELINES_PATH } from '../../common/constants';
-import { TIMELINES, NOTES } from '../app/translations';
+import { TIMELINES } from '../app/translations';
import type { LinkItem } from '../common/links/types';
export const links: LinkItem = {
@@ -30,14 +30,5 @@ export const links: LinkItem = {
path: `${TIMELINES_PATH}/template`,
sideNavDisabled: true,
},
- {
- id: SecurityPageName.notesManagement,
- title: NOTES,
- description: i18n.translate('xpack.securitySolution.appLinks.notesManagementDescription', {
- defaultMessage: 'Visualize and delete notes.',
- }),
- path: `${TIMELINES_PATH}/notes`,
- experimentalKey: 'securitySolutionNotesEnabled',
- },
],
};
diff --git a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx
index c7e8cb9887efe..2151a2624aeb4 100644
--- a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx
@@ -17,7 +17,7 @@ import { appendSearch } from '../../common/components/link_to/helpers';
import { TIMELINES_PATH } from '../../../common/constants';
-const timelinesPagePath = `${TIMELINES_PATH}/:tabName(${TimelineTypeEnum.default}|${TimelineTypeEnum.template}|notes)`;
+const timelinesPagePath = `${TIMELINES_PATH}/:tabName(${TimelineTypeEnum.default}|${TimelineTypeEnum.template})`;
const timelinesDefaultPath = `${TIMELINES_PATH}/${TimelineTypeEnum.default}`;
export const Timelines = React.memo(() => (
diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
index 9d3c05c97b685..08cf46b41ad35 100644
--- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
@@ -41,7 +41,7 @@ export const TimelinesPage = React.memo(() => {
{indicesExist ? (
- {capabilitiesCanUserCRUD && tabName !== 'notes' ? (
+ {capabilitiesCanUserCRUD && (
{
- ) : null}
+ )}
{
setImportDataModalToggle={setImportDataModal}
title={i18n.ALL_TIMELINES_PANEL_TITLE}
data-test-subj="stateful-open-timeline"
- tabName={tabName}
/>
) : (
diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/lib/emulator_server.types.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/lib/emulator_server.types.ts
index 5d8cb5c1ee595..165f6d8ef49e3 100644
--- a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/lib/emulator_server.types.ts
+++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/lib/emulator_server.types.ts
@@ -31,7 +31,7 @@ export interface EmulatorServerPluginRegisterOptions = any>
- extends Omit, 'register'> {
+ extends Omit, 'register'> {
register: (options: EmulatorServerPluginRegisterOptions) => void | Promise;
name: string;
/**
diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts
index 66a20569bdee8..d63435a9ece4a 100644
--- a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts
@@ -14,11 +14,14 @@ import { ALL_PRODUCT_FEATURE_KEYS } from '@kbn/security-solution-features/keys';
import { turnOffPolicyProtectionsIfNotSupported } from './turn_off_policy_protections';
import { FleetPackagePolicyGenerator } from '../../../common/endpoint/data_generators/fleet_package_policy_generator';
import type { PolicyData } from '../../../common/endpoint/types';
-import type { PackagePolicyClient } from '@kbn/fleet-plugin/server';
-import type { PromiseResolvedValue } from '../../../common/endpoint/types/utility_types';
-import { ensureOnlyEventCollectionIsAllowed } from '../../../common/endpoint/models/policy_config_helpers';
+import {
+ ensureOnlyEventCollectionIsAllowed,
+ resetCustomNotifications,
+} from '../../../common/endpoint/models/policy_config_helpers';
import type { ProductFeaturesService } from '../../lib/product_features_service/product_features_service';
import { createProductFeaturesServiceMock } from '../../lib/product_features_service/mocks';
+import { merge } from 'lodash';
+import { DefaultPolicyNotificationMessage } from '../../../common/endpoint/models/policy_config';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { createEndpointFleetServicesFactoryMock } from '../services/fleet/endpoint_fleet_services_factory.mocks';
@@ -31,307 +34,433 @@ describe('Turn Off Policy Protections Migration', () => {
const callTurnOffPolicyProtections = () =>
turnOffPolicyProtectionsIfNotSupported(esClient, fleetServices, productFeatureService, logger);
+ const mockPolicyListResponse = (
+ { total, items, page }: { total?: number; items?: PolicyData[]; page?: number } = {
+ total: 1,
+ page: 2,
+ items: [],
+ }
+ ) => {
+ const packagePolicyListSrv = fleetServices.packagePolicy.list as jest.Mock;
+ return packagePolicyListSrv.mockResolvedValueOnce({
+ total,
+ page,
+ perPage: 1500,
+ items,
+ });
+ };
+
const generatePolicyMock = (
- policyGenerator: FleetPackagePolicyGenerator,
withDisabledProtections = false,
- withDisabledProtectionUpdates = true
+ withCustomProtectionUpdates = true,
+ withCustomNotifications = true
): PolicyData => {
- const policy = policyGenerator.generateEndpointPackagePolicy();
-
- if (!withDisabledProtections && withDisabledProtectionUpdates) {
- return policy;
- } else if (!withDisabledProtections && !withDisabledProtectionUpdates) {
- policy.inputs[0].config.policy.value.global_manifest_version = '2023-01-01';
- return policy;
- } else if (withDisabledProtections && !withDisabledProtectionUpdates) {
+ const policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy();
+ if (withDisabledProtections) {
policy.inputs[0].config.policy.value = ensureOnlyEventCollectionIsAllowed(
policy.inputs[0].config.policy.value
);
+ }
+ if (withCustomProtectionUpdates) {
policy.inputs[0].config.policy.value.global_manifest_version = '2023-01-01';
- return policy;
- } else {
- policy.inputs[0].config.policy.value = ensureOnlyEventCollectionIsAllowed(
- policy.inputs[0].config.policy.value
+ }
+ if (!withCustomNotifications) {
+ policy.inputs[0].config.policy.value = merge(
+ {},
+ policy.inputs[0].config.policy.value,
+ resetCustomNotifications()
+ );
+ } else if (withCustomNotifications) {
+ policy.inputs[0].config.policy.value = merge(
+ {},
+ policy.inputs[0].config.policy.value,
+ resetCustomNotifications('custom test')
);
- return policy; // This is the only one that shouldn't be updated since it has default values for disabled features
}
+ return policy;
};
+ // I’ve decided to keep the content hardcoded for better readability instead of generating it dynamically.
+ const generateExpectedPolicyMock = ({
+ defaultProtections,
+ defaultNotes,
+ defaultUpdates,
+ id,
+ }: {
+ id: string;
+ defaultProtections: boolean;
+ defaultNotes: boolean;
+ defaultUpdates: boolean;
+ }) =>
+ expect.arrayContaining([
+ expect.objectContaining({
+ id,
+ inputs: [
+ expect.objectContaining({
+ config: expect.objectContaining({
+ policy: expect.objectContaining({
+ value: expect.objectContaining({
+ global_manifest_version: defaultUpdates ? 'latest' : '2023-01-01',
+ linux: expect.objectContaining({
+ behavior_protection: expect.objectContaining({
+ mode: defaultProtections ? 'off' : 'prevent',
+ }),
+ memory_protection: expect.objectContaining({
+ mode: defaultProtections ? 'off' : 'prevent',
+ }),
+ malware: expect.objectContaining({
+ mode: defaultProtections ? 'off' : 'prevent',
+ }),
+ popup: expect.objectContaining({
+ behavior_protection: expect.objectContaining({
+ message: defaultNotes ? DefaultPolicyNotificationMessage : 'custom test',
+ }),
+ memory_protection: expect.objectContaining({
+ message: defaultNotes ? DefaultPolicyNotificationMessage : 'custom test',
+ }),
+ malware: expect.objectContaining({
+ message: defaultNotes ? DefaultPolicyNotificationMessage : 'custom test',
+ }),
+ }),
+ }),
+ mac: expect.objectContaining({
+ behavior_protection: expect.objectContaining({
+ mode: defaultProtections ? 'off' : 'prevent',
+ }),
+ memory_protection: expect.objectContaining({
+ mode: defaultProtections ? 'off' : 'prevent',
+ }),
+ malware: expect.objectContaining({
+ mode: defaultProtections ? 'off' : 'prevent',
+ }),
+ popup: expect.objectContaining({
+ behavior_protection: expect.objectContaining({
+ message: defaultNotes ? DefaultPolicyNotificationMessage : 'custom test',
+ }),
+ memory_protection: expect.objectContaining({
+ message: defaultNotes ? DefaultPolicyNotificationMessage : 'custom test',
+ }),
+ malware: expect.objectContaining({
+ message: defaultNotes ? DefaultPolicyNotificationMessage : 'custom test',
+ }),
+ }),
+ }),
+ windows: expect.objectContaining({
+ behavior_protection: expect.objectContaining({
+ mode: defaultProtections ? 'off' : 'prevent',
+ }),
+ memory_protection: expect.objectContaining({
+ mode: defaultProtections ? 'off' : 'prevent',
+ }),
+ malware: expect.objectContaining({
+ mode: defaultProtections ? 'off' : 'prevent',
+ }),
+ ransomware: expect.objectContaining({
+ mode: defaultProtections ? 'off' : 'prevent',
+ }),
+ popup: expect.objectContaining({
+ behavior_protection: expect.objectContaining({
+ message: defaultNotes ? DefaultPolicyNotificationMessage : 'custom test',
+ }),
+ memory_protection: expect.objectContaining({
+ message: defaultNotes ? DefaultPolicyNotificationMessage : 'custom test',
+ }),
+ malware: expect.objectContaining({
+ message: defaultNotes ? DefaultPolicyNotificationMessage : 'custom test',
+ }),
+ ransomware: expect.objectContaining({
+ message: defaultNotes ? DefaultPolicyNotificationMessage : 'custom test',
+ }),
+ }),
+ }),
+ }),
+ }),
+ }),
+ }),
+ ],
+ }),
+ ]);
+
+ const createFilteredProductFeaturesServiceMock = (
+ keysToExclude: string[] = [
+ 'endpoint_policy_protections',
+ 'endpoint_custom_notification',
+ 'endpoint_protection_updates',
+ ]
+ ) =>
+ createProductFeaturesServiceMock(
+ ALL_PRODUCT_FEATURE_KEYS.filter((key) => !keysToExclude.includes(key))
+ );
+
beforeEach(() => {
const endpointContextStartContract = createMockEndpointAppContextServiceStartContract();
logger = loggingSystemMock.createLogger();
({ esClient } = endpointContextStartContract);
- productFeatureService = endpointContextStartContract.productFeaturesService;
-
- fleetServices = createEndpointFleetServicesFactoryMock().service.asInternalUser();
- });
+ productFeatureService = createFilteredProductFeaturesServiceMock();
- describe('and both `endpointPolicyProtections` and `endpointProtectionUpdates` is enabled', () => {
- it('should do nothing', async () => {
- await callTurnOffPolicyProtections();
+ // productFeatureService = endpointContextStartContract.productFeaturesService;
- expect(fleetServices.packagePolicy.list as jest.Mock).not.toHaveBeenCalled();
- expect(logger.info).toHaveBeenNthCalledWith(
- 1,
- 'App feature [endpoint_policy_protections] is enabled. Nothing to do!'
- );
- expect(logger.info).toHaveBeenLastCalledWith(
- 'App feature [endpoint_protection_updates] is enabled. Nothing to do!'
- );
- });
+ fleetServices = createEndpointFleetServicesFactoryMock().service.asInternalUser();
});
- describe('and `endpointProtectionUpdates` is disabled but `endpointPolicyProtections` is enabled', () => {
- let policyGenerator: FleetPackagePolicyGenerator;
- let page1Items: PolicyData[] = [];
- let page2Items: PolicyData[] = [];
- let bulkUpdateResponse: PromiseResolvedValue>;
-
+ describe('when merging policy updates for different product features', () => {
beforeEach(() => {
- policyGenerator = new FleetPackagePolicyGenerator('seed');
- const packagePolicyListSrv = fleetServices.packagePolicy.list as jest.Mock;
-
- productFeatureService = createProductFeaturesServiceMock(
- ALL_PRODUCT_FEATURE_KEYS.filter((key) => key !== 'endpoint_protection_updates')
- );
-
- page1Items = [
- generatePolicyMock(policyGenerator, false),
- generatePolicyMock(policyGenerator, false, false),
- ];
- page2Items = [
- generatePolicyMock(policyGenerator, false, false),
- generatePolicyMock(policyGenerator, false),
- ];
-
- packagePolicyListSrv
- .mockImplementationOnce(async () => {
- return {
- total: 1500,
- page: 1,
- perPage: 1000,
- items: page1Items,
- };
- })
- .mockImplementationOnce(async () => {
- return {
- total: 1500,
- page: 2,
- perPage: 1000,
- items: page2Items,
- };
- });
-
- bulkUpdateResponse = {
- updatedPolicies: [page1Items[1], page2Items[0]],
+ // We only check for the `bulkUpdate` call, so we mock it to avoid side effects
+ fleetServices.packagePolicy.bulkUpdate = jest.fn().mockResolvedValue({
+ updatedPolicies: [],
failedPolicies: [],
- };
-
- (fleetServices.packagePolicy.bulkUpdate as jest.Mock).mockImplementation(async () => {
- return bulkUpdateResponse;
});
});
+ describe('All policies compliant', () => {
+ it('should not update if all all features are enabled', async () => {
+ productFeatureService = createProductFeaturesServiceMock();
- afterEach(() => {
- jest.clearAllMocks();
- });
+ const items = [generatePolicyMock(false, true, true)]; // Custom protections, custom manifest, custom notifications set
- it('should update only policies that have non default manifest versions set', async () => {
- await callTurnOffPolicyProtections();
+ mockPolicyListResponse({ items });
- expect(fleetServices.packagePolicy.list as jest.Mock).toHaveBeenCalledTimes(2);
- expect(fleetServices.packagePolicy.bulkUpdate as jest.Mock).toHaveBeenCalledWith(
- fleetServices.savedObjects.createInternalScopedSoClient({ readonly: false }),
- esClient,
- [
- expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![0].id }),
- expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![1].id }),
- ],
- { user: { username: 'elastic' } }
- );
- });
- });
+ await callTurnOffPolicyProtections();
- describe('and `endpointPolicyProtections` is disabled, but `endpointProtectionUpdates` is enabled', () => {
- let policyGenerator: FleetPackagePolicyGenerator;
- let page1Items: PolicyData[] = [];
- let page2Items: PolicyData[] = [];
- let bulkUpdateResponse: PromiseResolvedValue>;
+ expect(fleetServices.packagePolicy.bulkUpdate).not.toHaveBeenCalled();
+ });
- beforeEach(() => {
- policyGenerator = new FleetPackagePolicyGenerator('seed');
- const packagePolicyListSrv = fleetServices.packagePolicy.list as jest.Mock;
+ it('should not update if all policies are compliant', async () => {
+ const items = [generatePolicyMock(true, false, false)]; // Custom protections, default manifest, default notifications set
- productFeatureService = createProductFeaturesServiceMock(
- ALL_PRODUCT_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections')
- );
+ mockPolicyListResponse({ items });
- page1Items = [
- generatePolicyMock(policyGenerator, false, false),
- generatePolicyMock(policyGenerator, true, false),
- ];
- page2Items = [
- generatePolicyMock(policyGenerator, true, false),
- generatePolicyMock(policyGenerator, false, false),
- ];
-
- packagePolicyListSrv
- .mockImplementationOnce(async () => {
- return {
- total: 1500,
- page: 1,
- perPage: 1000,
- items: page1Items,
- };
- })
- .mockImplementationOnce(async () => {
- return {
- total: 1500,
- page: 2,
- perPage: 1000,
- items: page2Items,
- };
- });
-
- bulkUpdateResponse = {
- updatedPolicies: [page1Items[0], page2Items[1]],
- failedPolicies: [],
- };
+ await callTurnOffPolicyProtections();
- (fleetServices.packagePolicy.bulkUpdate as jest.Mock).mockImplementation(async () => {
- return bulkUpdateResponse;
+ expect(fleetServices.packagePolicy.bulkUpdate).not.toHaveBeenCalled();
});
});
- afterEach(() => {
- jest.clearAllMocks();
- });
+ describe('Single feature not compliant with product features', () => {
+ it('should update properly if only `endpointPolicyProtections` changed', async () => {
+ const items = [
+ generatePolicyMock(false, false, false), // Custom protections, default manifest, default notifications set
+ generatePolicyMock(true, false, false), // Compliant policy
+ ];
+
+ mockPolicyListResponse({ items });
+
+ await callTurnOffPolicyProtections();
+
+ const mockCalls = (fleetServices.packagePolicy.bulkUpdate as jest.Mock).mock.calls;
+ expect(mockCalls.length).toBeGreaterThan(0);
+ const mockArguments = mockCalls[0][2];
+ expect(mockArguments.length).toBe(1); // Only one policy should be updated
+ expect(mockArguments).toEqual(
+ generateExpectedPolicyMock({
+ id: items[0].id, // Only the first policy should be updated
+ defaultProtections: true,
+ defaultNotes: true,
+ defaultUpdates: true,
+ })
+ );
+ });
- it('should update only policies that have protections turn on', async () => {
- await callTurnOffPolicyProtections();
+ it('should update properly if only `endpointPolicyProtections` changed across 2 result pages', async () => {
+ const packagePolicyListSrv = fleetServices.packagePolicy.list as jest.Mock;
+
+ const allPolicies = [
+ generatePolicyMock(false, false, false), // Custom protections, default manifest, default notifications set
+ generatePolicyMock(false, false, false), // Custom protections, default manifest, default notifications set
+ ];
+
+ mockPolicyListResponse({ total: 1500, items: [allPolicies[0]] });
+ mockPolicyListResponse({ total: 1500, items: [allPolicies[1]] });
+
+ await callTurnOffPolicyProtections();
+ expect(packagePolicyListSrv).toHaveBeenCalledTimes(2);
+ expect(fleetServices.packagePolicy.bulkUpdate).toHaveBeenCalledTimes(1);
+
+ const mockCalls = (fleetServices.packagePolicy.bulkUpdate as jest.Mock).mock.calls;
+ const mockArguments = mockCalls[0][2];
+ expect(mockArguments.length).toBe(2);
+ expect(mockArguments[0].inputs[0].config.policy.value.global_manifest_version).toBe(
+ 'latest'
+ );
+ expect(mockArguments[1].inputs[0].config.policy.value.windows.ransomware.mode).toBe('off');
+ expect(mockArguments[0].inputs[0].config.policy.value.windows.ransomware.mode).toBe('off');
+ });
- expect(fleetServices.packagePolicy.list as jest.Mock).toHaveBeenCalledTimes(2);
- expect(fleetServices.packagePolicy.bulkUpdate as jest.Mock).toHaveBeenCalledWith(
- fleetServices.savedObjects.createInternalScopedSoClient({ readonly: false }),
- esClient,
- [
- expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![0].id }),
- expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![1].id }),
- ],
- { user: { username: 'elastic' } }
- );
- expect(logger.info).toHaveBeenCalledWith(
- 'Found 2 policies that need updates:\n' +
- `Policy [${bulkUpdateResponse.updatedPolicies![0].id}][${
- bulkUpdateResponse.updatedPolicies![0].name
- }] updated to disable protections. Trigger: [property [mac.malware.mode] is set to [prevent]]\n` +
- `Policy [${bulkUpdateResponse.updatedPolicies![1].id}][${
- bulkUpdateResponse.updatedPolicies![1].name
- }] updated to disable protections. Trigger: [property [mac.malware.mode] is set to [prevent]]`
- );
- expect(logger.info).toHaveBeenCalledWith('Done. All updates applied successfully');
- });
+ it('should update properly if only `endpointProtectionUpdates` not compliant', async () => {
+ const allPolicies = [
+ generatePolicyMock(true, false, false), // Compliant policy
+ generatePolicyMock(true, true, false), // Default protections, custom manifest, default notifications set
+ ];
+
+ mockPolicyListResponse({ total: 2, items: allPolicies });
+
+ await callTurnOffPolicyProtections();
+
+ const mockCalls = (fleetServices.packagePolicy.bulkUpdate as jest.Mock).mock.calls;
+ expect(mockCalls.length).toBeGreaterThan(0);
+ const mockArguments = mockCalls[0][2];
+ expect(mockArguments.length).toBe(1); // Only one policy should be updated
+ expect(mockArguments).toEqual(
+ generateExpectedPolicyMock({
+ id: allPolicies[1].id, // Only the second policy should be updated
+ defaultProtections: true,
+ defaultNotes: true,
+ defaultUpdates: true,
+ })
+ );
+ });
- it('should log failures', async () => {
- bulkUpdateResponse.failedPolicies.push({
- error: new Error('oh oh'),
- packagePolicy: bulkUpdateResponse.updatedPolicies![0],
+ it('should update properly if only `endpointCustomNote` not compliant', async () => {
+ const allPolicies = [
+ generatePolicyMock(true, false, true), // Default protections, default manifest, custom notifications set
+ generatePolicyMock(true, false, false), // Compliant policy
+ ];
+
+ mockPolicyListResponse({ total: 2, items: allPolicies });
+
+ await callTurnOffPolicyProtections();
+
+ const mockCalls = (fleetServices.packagePolicy.bulkUpdate as jest.Mock).mock.calls;
+ expect(mockCalls.length).toBeGreaterThan(0);
+ const mockArguments = mockCalls[0][2];
+ expect(mockArguments.length).toBe(1); // Only one policy should be updated
+ expect(mockArguments).toEqual(
+ generateExpectedPolicyMock({
+ id: allPolicies[0].id, // Only the first policy should be updated
+ defaultProtections: true,
+ defaultNotes: true,
+ defaultUpdates: true,
+ })
+ );
});
- await callTurnOffPolicyProtections();
- expect(logger.error).toHaveBeenCalledWith(
- expect.stringContaining('Done. 1 out of 2 failed to update:')
- );
+ it('should update properly if only `endpointCustomNote` and `endpointProtectionUpdates` not compliant across 2 policies', async () => {
+ const allPolicies = [
+ generatePolicyMock(true, false, true), // Default protections, default manifest, custom notifications set
+ generatePolicyMock(true, true, false), // Default protections, custom manifest, default notifications set
+ generatePolicyMock(true, false, false), // Default protections, default manifest, default notifications set
+ ];
+
+ mockPolicyListResponse({ total: 3, items: allPolicies });
+
+ await callTurnOffPolicyProtections();
+
+ const mockCalls = (fleetServices.packagePolicy.bulkUpdate as jest.Mock).mock.calls;
+ expect(mockCalls.length).toBeGreaterThan(0);
+ const mockArguments = mockCalls[0][2];
+ expect(mockArguments.length).toBe(2); // Two policies should be updated
+ expect(mockArguments[0].inputs[0].config.policy.value.global_manifest_version).toBe(
+ 'latest'
+ );
+ expect(mockArguments[1].inputs[0].config.policy.value.global_manifest_version).toBe(
+ 'latest'
+ );
+ expect(
+ mockArguments[0].inputs[0].config.policy.value.windows.popup.memory_protection.message
+ ).toBe(DefaultPolicyNotificationMessage);
+ expect(
+ mockArguments[1].inputs[0].config.policy.value.windows.popup.memory_protection.message
+ ).toBe(DefaultPolicyNotificationMessage);
+ });
});
- });
-
- describe('and both `endpointPolicyProtections` and `endpointProtectionUpdates` is disabled', () => {
- let policyGenerator: FleetPackagePolicyGenerator;
- let page1Items: PolicyData[] = [];
- let page2Items: PolicyData[] = [];
- let bulkUpdateResponse: PromiseResolvedValue>;
-
- beforeEach(() => {
- policyGenerator = new FleetPackagePolicyGenerator('seed');
- const packagePolicyListSrv = fleetServices.packagePolicy.list as jest.Mock;
-
- productFeatureService = createProductFeaturesServiceMock(
- ALL_PRODUCT_FEATURE_KEYS.filter(
- (key) => key !== 'endpoint_policy_protections' && key !== 'endpoint_protection_updates'
- )
- );
- page1Items = [
- generatePolicyMock(policyGenerator),
- generatePolicyMock(policyGenerator, true), // This is the only one that shouldn't be updated since it has default values for disabled features
- generatePolicyMock(policyGenerator, true, false),
- generatePolicyMock(policyGenerator, false, false),
- ];
-
- page2Items = [
- generatePolicyMock(policyGenerator, false, false),
- generatePolicyMock(policyGenerator, true, false),
- generatePolicyMock(policyGenerator, true), // This is the only one that shouldn't be updated since it has default values for disabled features
- generatePolicyMock(policyGenerator),
- ];
-
- packagePolicyListSrv
- .mockImplementationOnce(async () => {
- return {
- total: 1500,
- page: 1,
- perPage: 1000,
- items: page1Items,
- };
- })
- .mockImplementationOnce(async () => {
- return {
- total: 1500,
- page: 2,
- perPage: 1000,
- items: page2Items,
- };
- });
-
- bulkUpdateResponse = {
- updatedPolicies: [
- page1Items[0],
- page1Items[2],
- page1Items[3],
- page2Items[0],
- page2Items[1],
- page2Items[3],
- ],
- failedPolicies: [],
+ describe('Multiple features not compliant with product features', () => {
+ const verifyGeneratedPolicies = (policies: PolicyData[]) => {
+ expect(policies[0].inputs[0].config.policy.value.global_manifest_version).toBe(
+ '2023-01-01'
+ );
+ expect(policies[0].inputs[0].config.policy.value.windows.memory_protection.mode).toBe(
+ 'prevent'
+ );
+ expect(
+ policies[0].inputs[0].config.policy.value.windows.popup.memory_protection.message
+ ).toBe('custom test');
};
- (fleetServices.packagePolicy.bulkUpdate as jest.Mock).mockImplementation(async () => {
- return bulkUpdateResponse;
+ it('should merge updates for `endpointPolicyProtections` and `endpointCustomNotification`', async () => {
+ productFeatureService = createFilteredProductFeaturesServiceMock([
+ 'endpoint_custom_notification',
+ 'endpoint_policy_protections',
+ ]);
+
+ const policiesNeedingUpdate = [
+ generatePolicyMock(false, true, true), // Custom protections, custom manifest, custom notifications set
+ ];
+
+ // Sanity check to ensure the policies are generated correctly
+ verifyGeneratedPolicies(policiesNeedingUpdate);
+
+ mockPolicyListResponse({ total: 1, items: policiesNeedingUpdate });
+
+ await callTurnOffPolicyProtections();
+
+ expect(fleetServices.packagePolicy.bulkUpdate).toHaveBeenCalledWith(
+ fleetServices.savedObjects.createInternalScopedSoClient({ readonly: false }),
+ esClient,
+ generateExpectedPolicyMock({
+ id: policiesNeedingUpdate[0].id,
+ defaultProtections: true,
+ defaultNotes: true,
+ defaultUpdates: false,
+ }),
+ { user: { username: 'elastic' } }
+ );
});
- });
- afterEach(() => {
- jest.clearAllMocks();
- });
+ it('should merge updates for `endpointProtectionUpdates` and `endpointCustomNotification`', async () => {
+ productFeatureService = createFilteredProductFeaturesServiceMock([
+ 'endpoint_custom_notification',
+ 'endpoint_protection_updates',
+ ]);
+
+ const policiesNeedingUpdate = [
+ generatePolicyMock(false, true, true), // Custom protections, custom manifest, custom notifications set
+ ];
+
+ // Sanity check to ensure the policies are generated correctly
+ verifyGeneratedPolicies(policiesNeedingUpdate);
+
+ mockPolicyListResponse({ total: 1, items: policiesNeedingUpdate });
+
+ await callTurnOffPolicyProtections();
+
+ expect(fleetServices.packagePolicy.bulkUpdate).toHaveBeenCalledWith(
+ fleetServices.savedObjects.createInternalScopedSoClient({ readonly: false }),
+ esClient,
+ generateExpectedPolicyMock({
+ id: policiesNeedingUpdate[0].id,
+ defaultProtections: false,
+ defaultNotes: true,
+ defaultUpdates: true,
+ }),
+ { user: { username: 'elastic' } }
+ );
+ });
- it('should update only policies that have protections and protection updates turned on', async () => {
- await callTurnOffPolicyProtections();
-
- expect(fleetServices.packagePolicy.list as jest.Mock).toHaveBeenCalledTimes(2);
- expect(fleetServices.packagePolicy.bulkUpdate as jest.Mock).toHaveBeenCalledWith(
- fleetServices.savedObjects.createInternalUnscopedSoClient(),
- esClient,
- [
- expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![0].id }),
- expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![1].id }),
- expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![2].id }),
- expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![3].id }),
- expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![4].id }),
- expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![5].id }),
- ],
- { user: { username: 'elastic' } }
- );
+ it('should merge updates for `endpointPolicyProtections`, `endpointProtectionUpdates`, and `endpointCustomNotification`', async () => {
+ const policiesNeedingUpdate = [
+ generatePolicyMock(false, true, true), // Custom protections, custom manifest, custom notifications set
+ ];
+
+ // Sanity check to ensure the policies are generated correctly
+ verifyGeneratedPolicies(policiesNeedingUpdate);
+
+ mockPolicyListResponse({ total: 1, items: policiesNeedingUpdate });
+
+ await callTurnOffPolicyProtections();
+
+ expect(fleetServices.packagePolicy.bulkUpdate).toHaveBeenCalledWith(
+ fleetServices.savedObjects.createInternalUnscopedSoClient(),
+ esClient,
+ generateExpectedPolicyMock({
+ id: policiesNeedingUpdate[0].id,
+ defaultProtections: true,
+ defaultNotes: true,
+ defaultUpdates: true,
+ }),
+ { user: { username: 'elastic' } }
+ );
+ });
});
});
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts
index 43e30b25b4168..ffc7141a6979c 100644
--- a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts
@@ -9,9 +9,12 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import type { UpdatePackagePolicy } from '@kbn/fleet-plugin/common';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys';
+import { merge } from 'lodash';
import {
+ checkIfPopupMessagesContainCustomNotifications,
ensureOnlyEventCollectionIsAllowed,
isPolicySetToEventCollectionOnly,
+ resetCustomNotifications,
} from '../../../common/endpoint/models/policy_config_helpers';
import type { PolicyData } from '../../../common/endpoint/types';
import type { EndpointInternalFleetServicesInterface } from '../services/fleet';
@@ -24,63 +27,44 @@ export const turnOffPolicyProtectionsIfNotSupported = async (
productFeaturesService: ProductFeaturesService,
logger: Logger
): Promise => {
- const log = logger.get('endpoint', 'policyProtections');
+ const log = logger.get('endpoint', 'policyProtectionsComplianceChecks');
const isProtectionUpdatesFeatureEnabled = productFeaturesService.isEnabled(
ProductFeatureSecurityKey.endpointProtectionUpdates
);
-
const isPolicyProtectionsEnabled = productFeaturesService.isEnabled(
ProductFeatureSecurityKey.endpointPolicyProtections
);
+ const isCustomNotificationEnabled = productFeaturesService.isEnabled(
+ ProductFeatureSecurityKey.endpointCustomNotification
+ );
- if (isPolicyProtectionsEnabled) {
- log.info(
- `App feature [${ProductFeatureSecurityKey.endpointPolicyProtections}] is enabled. Nothing to do!`
- );
- }
-
- if (isProtectionUpdatesFeatureEnabled) {
- log.info(
- `App feature [${ProductFeatureSecurityKey.endpointProtectionUpdates}] is enabled. Nothing to do!`
- );
- }
-
- if (isPolicyProtectionsEnabled && isProtectionUpdatesFeatureEnabled) {
+ if (
+ isPolicyProtectionsEnabled &&
+ isProtectionUpdatesFeatureEnabled &&
+ isCustomNotificationEnabled
+ ) {
+ log.info('All relevant features are enabled. Nothing to do!');
return;
}
- if (!isPolicyProtectionsEnabled) {
- log.info(
- `App feature [${ProductFeatureSecurityKey.endpointPolicyProtections}] is disabled. Checking endpoint integration policies for compliance`
- );
- }
-
- if (!isProtectionUpdatesFeatureEnabled) {
- log.info(
- `App feature [${ProductFeatureSecurityKey.endpointProtectionUpdates}] is disabled. Checking endpoint integration policies for compliance`
- );
- }
-
const { packagePolicy, savedObjects, endpointPolicyKuery } = fleetServices;
const internalSoClient = savedObjects.createInternalScopedSoClient({ readonly: false });
const updates: UpdatePackagePolicy[] = [];
const messages: string[] = [];
const perPage = 1000;
let hasMoreData = true;
- let total = 0;
let page = 1;
- do {
- const currentPage = page++;
- const { items, total: totalPolicies } = await packagePolicy.list(internalSoClient, {
- page: currentPage,
+ while (hasMoreData) {
+ const { items, total } = await packagePolicy.list(internalSoClient, {
+ page,
kuery: endpointPolicyKuery,
perPage,
});
- total = totalPolicies;
- hasMoreData = currentPage * perPage < total;
+ hasMoreData = page * perPage < total;
+ page++;
for (const item of items) {
const integrationPolicy = item as PolicyData;
@@ -88,56 +72,58 @@ export const turnOffPolicyProtectionsIfNotSupported = async (
const { message, isOnlyCollectingEvents } = isPolicySetToEventCollectionOnly(policySettings);
const shouldDowngradeProtectionUpdates =
!isProtectionUpdatesFeatureEnabled && policySettings.global_manifest_version !== 'latest';
-
- if (!isPolicyProtectionsEnabled && !isOnlyCollectingEvents) {
- messages.push(
- `Policy [${integrationPolicy.id}][${integrationPolicy.name}] updated to disable protections. Trigger: [${message}]`
- );
-
+ const shouldDowngradeCustomNotifications =
+ !isCustomNotificationEnabled &&
+ checkIfPopupMessagesContainCustomNotifications(policySettings);
+
+ if (
+ (!isPolicyProtectionsEnabled && !isOnlyCollectingEvents) ||
+ shouldDowngradeProtectionUpdates ||
+ shouldDowngradeCustomNotifications
+ ) {
+ if (!isPolicyProtectionsEnabled && !isOnlyCollectingEvents) {
+ messages.push(
+ `Policy [${integrationPolicy.id}][${integrationPolicy.name}] updated to disable protections. Trigger: [${message}]`
+ );
+ }
if (shouldDowngradeProtectionUpdates) {
messages.push(
`Policy [${integrationPolicy.id}][${integrationPolicy.name}] updated to downgrade protection updates.`
);
}
+ if (shouldDowngradeCustomNotifications) {
+ messages.push(
+ `Policy [${integrationPolicy.id}][${integrationPolicy.name}] updated to reset custom notifications.`
+ );
+ }
- integrationPolicy.inputs[0].config.policy.value = {
- ...ensureOnlyEventCollectionIsAllowed(policySettings),
- ...(shouldDowngradeProtectionUpdates ? { global_manifest_version: 'latest' } : {}),
- };
-
- updates.push({
- ...getPolicyDataForUpdate(integrationPolicy),
- id: integrationPolicy.id,
- });
- } else if (shouldDowngradeProtectionUpdates) {
- messages.push(
- `Policy [${integrationPolicy.id}][${integrationPolicy.name}] updated to downgrade protection updates.`
+ integrationPolicy.inputs[0].config.policy.value = merge(
+ {},
+ policySettings,
+ !isPolicyProtectionsEnabled && !isOnlyCollectingEvents
+ ? ensureOnlyEventCollectionIsAllowed(policySettings)
+ : {},
+ shouldDowngradeProtectionUpdates ? { global_manifest_version: 'latest' } : {},
+ shouldDowngradeCustomNotifications ? resetCustomNotifications() : {}
);
- integrationPolicy.inputs[0].config.policy.value.global_manifest_version = 'latest';
-
updates.push({
...getPolicyDataForUpdate(integrationPolicy),
id: integrationPolicy.id,
});
}
}
- } while (hasMoreData);
+ }
if (updates.length > 0) {
log.info(`Found ${updates.length} policies that need updates:\n${messages.join('\n')}`);
-
const bulkUpdateResponse = await fleetServices.packagePolicy.bulkUpdate(
internalSoClient,
esClient,
updates,
- {
- user: { username: 'elastic' } as AuthenticatedUser,
- }
+ { user: { username: 'elastic' } as AuthenticatedUser }
);
-
log.debug(() => `Bulk update response:\n${JSON.stringify(bulkUpdateResponse, null, 2)}`);
-
if (bulkUpdateResponse.failedPolicies.length > 0) {
log.error(
`Done. ${bulkUpdateResponse.failedPolicies.length} out of ${
@@ -145,9 +131,9 @@ export const turnOffPolicyProtectionsIfNotSupported = async (
} failed to update:\n${JSON.stringify(bulkUpdateResponse.failedPolicies, null, 2)}`
);
} else {
- log.info(`Done. All updates applied successfully`);
+ log.info('Done. All updates applied successfully');
}
} else {
- log.info(`Done. Checked ${total} policies and no updates needed`);
+ log.info(`Done. Checked ${page * perPage} policies and no updates needed`);
}
};
diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts
index 81bfbf1cdd979..6accb29354ee4 100644
--- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts
+++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts
@@ -600,7 +600,29 @@ describe('ingest_integration tests ', () => {
await expect(() =>
callback(policyConfig, soClient, esClient, requestContextMock.convertContext(ctx), req)
).rejects.toThrow(
- 'To modify protection updates, you must add at least Endpoint Complete to your project.'
+ 'To modify protection updates, you must add Endpoint Complete to your project.'
+ );
+ });
+
+ it('should throw if endpointCustomNotification productFeature is disabled and user modifies popup.[protection].message', async () => {
+ productFeaturesService = createProductFeaturesServiceMock(
+ ALL_PRODUCT_FEATURE_KEYS.filter((key) => key !== 'endpoint_custom_notification')
+ );
+ const callback = getPackagePolicyUpdateCallback(
+ logger,
+ licenseService,
+ endpointAppContextStartContract.featureUsageService,
+ endpointMetadataService,
+ cloudService,
+ esClient,
+ productFeaturesService
+ );
+ const policyConfig = generator.generatePolicyPackagePolicy();
+ policyConfig.inputs[0]!.config!.policy.value.windows.popup.ransomware.message = 'foo';
+ await expect(() =>
+ callback(policyConfig, soClient, esClient, requestContextMock.convertContext(ctx), req)
+ ).rejects.toThrow(
+ 'To customize the user notification, you must add Endpoint Protection Complete to your project.'
);
});
diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_policy_against_product_features.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_policy_against_product_features.ts
index 78db424deaa01..e477006b3dfdc 100644
--- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_policy_against_product_features.ts
+++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_policy_against_product_features.ts
@@ -7,6 +7,7 @@
import type { NewPackagePolicyInput } from '@kbn/fleet-plugin/common';
import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys';
+import { checkIfPopupMessagesContainCustomNotifications } from '../../../common/endpoint/models/policy_config_helpers';
import type { ProductFeaturesService } from '../../lib/product_features_service';
export const validatePolicyAgainstProductFeatures = (
@@ -14,15 +15,36 @@ export const validatePolicyAgainstProductFeatures = (
productFeaturesService: ProductFeaturesService
): void => {
const input = inputs.find((i) => i.type === 'endpoint');
- if (input?.config?.policy?.value?.global_manifest_version) {
- const globalManifestVersion = input.config.policy.value.global_manifest_version;
+ const policySettings = input?.config?.policy?.value;
+
+ if (policySettings) {
+ const globalManifestVersion = policySettings.global_manifest_version;
+
+ if (globalManifestVersion) {
+ if (
+ globalManifestVersion !== 'latest' &&
+ !productFeaturesService.isEnabled(ProductFeatureSecurityKey.endpointProtectionUpdates)
+ ) {
+ const productFeatureError: Error & { statusCode?: number; apiPassThrough?: boolean } =
+ new Error(
+ 'To modify protection updates, you must add Endpoint Complete to your project.'
+ );
+ productFeatureError.statusCode = 403;
+ productFeatureError.apiPassThrough = true;
+ throw productFeatureError;
+ }
+ }
+
+ const popupMessagesContainCustomNotifications =
+ checkIfPopupMessagesContainCustomNotifications(policySettings);
+
if (
- globalManifestVersion !== 'latest' &&
- !productFeaturesService.isEnabled(ProductFeatureSecurityKey.endpointProtectionUpdates)
+ popupMessagesContainCustomNotifications &&
+ !productFeaturesService.isEnabled(ProductFeatureSecurityKey.endpointCustomNotification)
) {
const productFeatureError: Error & { statusCode?: number; apiPassThrough?: boolean } =
new Error(
- 'To modify protection updates, you must add at least Endpoint Complete to your project.'
+ 'To customize the user notification, you must add Endpoint Protection Complete to your project.'
);
productFeatureError.statusCode = 403;
productFeatureError.apiPassThrough = true;
diff --git a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
index e817679db1c31..6d3155f1b6ad1 100644
--- a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
+++ b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
@@ -42,6 +42,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
ProductFeatureKey.endpointResponseActions,
ProductFeatureKey.osqueryAutomatedResponseActions,
ProductFeatureKey.endpointAgentTamperProtection,
+ ProductFeatureKey.endpointCustomNotification,
ProductFeatureKey.endpointProtectionUpdates,
],
},
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_custom_notification.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_custom_notification.tsx
new file mode 100644
index 0000000000000..79af4662e7fe2
--- /dev/null
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_custom_notification.tsx
@@ -0,0 +1,64 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo } from 'react';
+import { EuiCard, EuiIcon, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import styled from '@emotion/styled';
+
+const CARD_TITLE = i18n.translate(
+ 'xpack.securitySolutionServerless.endpointCustomNotification.cardTitle',
+ {
+ defaultMessage: 'Custom notification',
+ }
+);
+const CARD_MESSAGE = i18n.translate(
+ 'xpack.securitySolutionServerless.endpointCustomNotification.cardMessage',
+ {
+ defaultMessage:
+ 'To customize the user notification, you must add Endpoint Protection Complete to your project.',
+ }
+);
+const BADGE_TEXT = i18n.translate(
+ 'xpack.securitySolutionServerless.endpointCustomNotification.badgeText',
+ {
+ defaultMessage: 'Endpoint Complete',
+ }
+);
+
+const CardDescription = styled.p`
+ padding: 0 33.3%;
+`;
+
+/**
+ * Component displayed when a given product tier is not allowed to use endpoint policy protections.
+ */
+export const EndpointCustomNotification = memo(() => {
+ return (
+ <>
+
+ }
+ betaBadgeProps={{
+ 'data-test-subj': 'endpointPolicy-customNotificationLockedCard-badge',
+ label: BADGE_TEXT,
+ }}
+ title={
+
+ {CARD_TITLE}
+
+ }
+ >
+ {CARD_MESSAGE}
+
+ >
+ );
+});
+EndpointCustomNotification.displayName = 'EndpointCustomNotification';
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts
index 3858f3bc38029..6797cc0d29fbf 100644
--- a/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts
@@ -13,6 +13,12 @@ export const EndpointPolicyProtectionsLazy = lazy(() =>
}))
);
+export const EndpointCustomNotificationLazy = lazy(() =>
+ import('./endpoint_custom_notification').then(({ EndpointCustomNotification }) => ({
+ default: EndpointCustomNotification,
+ }))
+);
+
export const RuleDetailsEndpointExceptionsLazy = lazy(() =>
import('./rule_details_endpoint_exceptions').then(({ RuleDetailsEndpointExceptions }) => ({
default: RuleDetailsEndpointExceptions,
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx
index bbc873ef137e7..f61f429ff9e88 100644
--- a/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx
@@ -19,6 +19,7 @@ import React from 'react';
import { CloudSecurityPostureIntegrationPliBlockLazy } from './sections/cloud_security_posture';
import {
EndpointAgentTamperProtectionLazy,
+ EndpointCustomNotificationLazy,
EndpointPolicyProtectionsLazy,
EndpointProtectionUpdatesLazy,
RuleDetailsEndpointExceptionsLazy,
@@ -108,6 +109,11 @@ export const upsellingSections: UpsellingSections = [
pli: ProductFeatureKey.endpointPolicyProtections,
component: EndpointPolicyProtectionsLazy,
},
+ {
+ id: 'endpoint_custom_notification',
+ pli: ProductFeatureKey.endpointCustomNotification,
+ component: EndpointCustomNotificationLazy,
+ },
{
id: 'ruleDetailsEndpointExceptions',
pli: ProductFeatureKey.endpointExceptions,
diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_connector.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_connector.tsx
index 78d4da85ab909..2932ffc7bf79a 100644
--- a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_connector.tsx
+++ b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_connector.tsx
@@ -12,6 +12,7 @@ import {
EuiFlexItem,
EuiPageTemplate,
EuiPanel,
+ EuiForm,
EuiPopover,
EuiSpacer,
EuiText,
@@ -158,9 +159,11 @@ export const EditConnector: React.FC = () => {
-
-
-
+
+
+
+
+
diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_description.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_description.tsx
index 111d91b74d043..1749e1673e269 100644
--- a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_description.tsx
+++ b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_description.tsx
@@ -12,12 +12,11 @@ import {
EuiFlexItem,
EuiFlexGroup,
EuiFieldText,
- EuiForm,
EuiButton,
EuiSpacer,
EuiFormRow,
EuiText,
- EuiButtonEmpty,
+ EuiLink,
} from '@elastic/eui';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Connector } from '@kbn/search-connectors';
@@ -56,43 +55,48 @@ export const EditDescription: React.FC = ({ connector }) =
});
return (
-
-
-
- setIsEditing(true)}
- >
- {EDIT_LABEL}
-
- }
+
+ setIsEditing(true)}
+ role="button"
>
- {isEditing ? (
- setNewDescription(event.target.value)}
- value={newDescription || ''}
- />
- ) : (
-
- {connector.description}
-
- )}
-
+ {EDIT_LABEL}
+
+
+ }
+ fullWidth
+ >
+
+
+ {isEditing ? (
+ setNewDescription(event.target.value)}
+ value={newDescription || ''}
+ fullWidth
+ />
+ ) : (
+
+ {connector.description}
+
+ )}
{isEditing && (
- <>
+
-
+
= ({ connector }) =
- >
+
)}
-
-
+
+
);
};
diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx
index fe4b6bd7e9768..a6598b4de15ea 100644
--- a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx
+++ b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx
@@ -7,14 +7,7 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
-import {
- EuiFlexItem,
- EuiFlexGroup,
- EuiForm,
- EuiFormLabel,
- EuiIcon,
- EuiSuperSelect,
-} from '@elastic/eui';
+import { EuiFlexItem, EuiFlexGroup, EuiIcon, EuiFormRow, EuiSuperSelect } from '@elastic/eui';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Connector } from '@kbn/search-connectors';
import { useKibanaServices } from '../../hooks/use_kibana';
@@ -69,12 +62,13 @@ export const EditServiceType: React.FC = ({ connector }) =
});
return (
-
-
- {i18n.translate('xpack.serverlessSearch.connectors.serviceTypeLabel', {
- defaultMessage: 'Connector type',
- })}
-
+
= ({ connector }) =
onChange={(event) => mutate(event)}
options={options}
valueOfSelected={connector.service_type || undefined}
+ fullWidth
/>
-
+
);
};
diff --git a/x-pack/plugins/serverless_search/public/application/components/index_documents/documents.tsx b/x-pack/plugins/serverless_search/public/application/components/index_documents/documents.tsx
index d74c3a479a68a..0dc6e74d63ba6 100644
--- a/x-pack/plugins/serverless_search/public/application/components/index_documents/documents.tsx
+++ b/x-pack/plugins/serverless_search/public/application/components/index_documents/documents.tsx
@@ -63,7 +63,7 @@ export const IndexDocuments: React.FC = ({ indexName }) =>
docs={docs}
docsPerPage={pagination.pageSize ?? 10}
isLoading={false}
- mappings={mappingData?.mappings?.properties ?? {}}
+ mappings={mappingData ? { [indexName]: mappingData } : undefined}
meta={documentsMeta ?? DEFAULT_PAGINATION}
onPaginate={(pageIndex) => setPagination({ ...pagination, pageIndex })}
setDocsPerPage={(pageSize) => setPagination({ ...pagination, pageSize })}
diff --git a/x-pack/plugins/serverless_search/public/plugin.ts b/x-pack/plugins/serverless_search/public/plugin.ts
index 7953474a099bf..d06a104d01fcc 100644
--- a/x-pack/plugins/serverless_search/public/plugin.ts
+++ b/x-pack/plugins/serverless_search/public/plugin.ts
@@ -72,18 +72,22 @@ export class ServerlessSearchPlugin
},
}),
});
+
+ const homeTitle = i18n.translate('xpack.serverlessSearch.app.home.title', {
+ defaultMessage: 'Home',
+ });
+
if (useSearchHomepage) {
core.application.register({
id: 'serverlessHomeRedirect',
- title: i18n.translate('xpack.serverlessSearch.app.home.title', {
- defaultMessage: 'Home',
- }),
+ title: homeTitle,
appRoute: '/app/elasticsearch',
euiIconType: 'logoElastic',
category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
visibleIn: [],
async mount({}: AppMountParameters) {
const [coreStart] = await core.getStartServices();
+ coreStart.chrome.docTitle.change(homeTitle);
coreStart.application.navigateToApp('searchHomepage');
return () => {};
},
@@ -102,6 +106,7 @@ export class ServerlessSearchPlugin
const { renderApp } = await import('./application/elasticsearch');
const [coreStart, services] = await core.getStartServices();
docLinks.setDocLinks(coreStart.docLinks.links);
+ coreStart.chrome.docTitle.change(homeTitle);
let user: AuthenticatedUser | undefined;
try {
const response = await coreStart.security.authc.getCurrentUser();
@@ -114,11 +119,13 @@ export class ServerlessSearchPlugin
},
});
+ const connectorsTitle = i18n.translate('xpack.serverlessSearch.app.connectors.title', {
+ defaultMessage: 'Connectors',
+ });
+
core.application.register({
id: 'serverlessConnectors',
- title: i18n.translate('xpack.serverlessSearch.app.connectors.title', {
- defaultMessage: 'Connectors',
- }),
+ title: connectorsTitle,
appRoute: '/app/connectors',
euiIconType: 'logoElastic',
category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
@@ -126,8 +133,9 @@ export class ServerlessSearchPlugin
async mount({ element, history }: AppMountParameters) {
const { renderApp } = await import('./application/connectors');
const [coreStart, services] = await core.getStartServices();
-
+ coreStart.chrome.docTitle.change(connectorsTitle);
docLinks.setDocLinks(coreStart.docLinks.links);
+
return await renderApp(element, coreStart, { history, ...services }, queryClient);
},
});
diff --git a/x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/customize_avatar/__snapshots__/customize_space_avatar.test.tsx.snap
similarity index 100%
rename from x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap
rename to x-pack/plugins/spaces/public/management/components/customize_avatar/__snapshots__/customize_space_avatar.test.tsx.snap
diff --git a/x-pack/plugins/spaces/public/management/components/customize_avatar/customize_avatar.tsx b/x-pack/plugins/spaces/public/management/components/customize_avatar/customize_avatar.tsx
new file mode 100644
index 0000000000000..37daa938663e0
--- /dev/null
+++ b/x-pack/plugins/spaces/public/management/components/customize_avatar/customize_avatar.tsx
@@ -0,0 +1,106 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiDescribedFormGroup, EuiLoadingSpinner, EuiTitle } from '@elastic/eui';
+import React, { Component, lazy, Suspense } from 'react';
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+
+import { CustomizeSpaceAvatar } from './customize_space_avatar';
+import { getSpaceAvatarComponent } from '../../../space_avatar';
+import type { SpaceValidator } from '../../lib';
+import type { CustomizeSpaceFormValues } from '../../types';
+import { SectionPanel } from '../section_panel';
+
+// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
+const LazySpaceAvatar = lazy(() =>
+ getSpaceAvatarComponent().then((component) => ({ default: component }))
+);
+
+interface Props {
+ validator: SpaceValidator;
+ space: CustomizeSpaceFormValues;
+ onChange: (space: CustomizeSpaceFormValues) => void;
+ title?: string;
+}
+
+interface State {
+ customizingAvatar: boolean;
+ usingCustomIdentifier: boolean;
+}
+
+export class CustomizeAvatar extends Component {
+ public state = {
+ customizingAvatar: false,
+ usingCustomIdentifier: false,
+ };
+
+ public render() {
+ const { validator, space } = this.props;
+
+ return (
+
+
+
+
+
+
+ }
+ description={
+ <>
+
+ {i18n.translate('xpack.spaces.management.manageSpacePage.avatarDescription', {
+ defaultMessage: 'Choose how your space avatar appears across Kibana.',
+ })}
+
+ {space.avatarType === 'image' ? (
+ }>
+
+
+ ) : (
+ }>
+
+
+ )}
+ >
+ }
+ fullWidth
+ >
+
+
+
+ );
+ }
+
+ public onAvatarChange = (space: CustomizeSpaceFormValues) => {
+ this.props.onChange(space);
+ };
+}
diff --git a/x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.test.tsx b/x-pack/plugins/spaces/public/management/components/customize_avatar/customize_space_avatar.test.tsx
similarity index 100%
rename from x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.test.tsx
rename to x-pack/plugins/spaces/public/management/components/customize_avatar/customize_space_avatar.test.tsx
diff --git a/x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.tsx b/x-pack/plugins/spaces/public/management/components/customize_avatar/customize_space_avatar.tsx
similarity index 100%
rename from x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.tsx
rename to x-pack/plugins/spaces/public/management/components/customize_avatar/customize_space_avatar.tsx
diff --git a/x-pack/plugins/spaces/public/management/components/customize_avatar/index.ts b/x-pack/plugins/spaces/public/management/components/customize_avatar/index.ts
new file mode 100644
index 0000000000000..60d3168efc245
--- /dev/null
+++ b/x-pack/plugins/spaces/public/management/components/customize_avatar/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { CustomizeAvatar } from './customize_avatar';
diff --git a/x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space.test.tsx.snap
index d7527e300eece..fe9692a971d3f 100644
--- a/x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space.test.tsx.snap
+++ b/x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space.test.tsx.snap
@@ -38,7 +38,7 @@ exports[`renders correctly 1`] = `
-
-
- Choose how your space avatar appears across Kibana.
-
- }
- >
-
-
-
- }
- fullWidth={true}
- title={
-
-
-
-
-
- }
- >
-
-
`;
diff --git a/x-pack/plugins/spaces/public/management/components/customize_space/customize_space.tsx b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space.tsx
index f67e40df53ec3..a5761c34c97b0 100644
--- a/x-pack/plugins/spaces/public/management/components/customize_space/customize_space.tsx
+++ b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space.tsx
@@ -9,29 +9,22 @@ import {
EuiDescribedFormGroup,
EuiFieldText,
EuiFormRow,
- EuiLoadingSpinner,
EuiText,
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
import type { ChangeEvent } from 'react';
-import React, { Component, lazy, Suspense } from 'react';
+import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
-import { CustomizeSpaceAvatar } from './customize_space_avatar';
-import { getSpaceAvatarComponent, getSpaceColor, getSpaceInitials } from '../../../space_avatar';
+import { getSpaceColor, getSpaceInitials } from '../../../space_avatar';
import type { SpaceValidator } from '../../lib';
import { toSpaceIdentifier } from '../../lib';
import type { CustomizeSpaceFormValues } from '../../types';
import { SectionPanel } from '../section_panel';
-// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
-const LazySpaceAvatar = lazy(() =>
- getSpaceAvatarComponent().then((component) => ({ default: component }))
-);
-
interface Props {
validator: SpaceValidator;
space: CustomizeSpaceFormValues;
@@ -112,7 +105,7 @@ export class CustomizeSpace extends Component {
helpText={i18n.translate(
'xpack.spaces.management.manageSpacePage.spaceDescriptionHelpText',
{
- defaultMessage: 'The description appears on the space selection screen.',
+ defaultMessage: 'Appears on the space selection screen and spaces list.',
}
)}
{...validator.validateSpaceDescription(this.props.space)}
@@ -156,58 +149,6 @@ export class CustomizeSpace extends Component {
)}
-
-
-
-
-
-
- }
- description={
- <>
-
- {i18n.translate('xpack.spaces.management.manageSpacePage.avatarDescription', {
- defaultMessage: 'Choose how your space avatar appears across Kibana.',
- })}
-
- {space.avatarType === 'image' ? (
- }>
-
-
- ) : (
- }>
-
-
- )}
- >
- }
- fullWidth
- >
-
-
);
}
diff --git a/x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap
index fd56cf65620f1..9babe0f169992 100644
--- a/x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap
+++ b/x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap
@@ -2,8 +2,7 @@
exports[`EnabledFeatures renders as expected 1`] = `
@@ -26,14 +25,16 @@ exports[`EnabledFeatures renders as expected 1`] = `
>
,
+ "solutionView":
+
+ ,
}
}
/>
diff --git a/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.tsx b/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.tsx
index 36d0694953242..377f4c51ff0c5 100644
--- a/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.tsx
+++ b/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.tsx
@@ -5,14 +5,12 @@
* 2.0.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import type { FunctionComponent } from 'react';
import React from 'react';
import type { KibanaFeatureConfig } from '@kbn/features-plugin/public';
-import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
-import { useKibana } from '@kbn/kibana-react-plugin/public';
import { FeatureTable } from './feature_table';
import type { Space } from '../../../../common';
@@ -25,16 +23,8 @@ interface Props {
}
export const EnabledFeatures: FunctionComponent = (props) => {
- const { services } = useKibana();
- const canManageRoles = services.application?.capabilities.management?.security?.roles === true;
-
return (
-
+
@@ -49,25 +39,16 @@ export const EnabledFeatures: FunctionComponent = (props) => {
+ solutionView: (
+
-
- ) : (
-
+
),
}}
/>
diff --git a/x-pack/plugins/spaces/public/management/components/solution_view/solution_view.tsx b/x-pack/plugins/spaces/public/management/components/solution_view/solution_view.tsx
index c336791991df4..4bf5e437f7350 100644
--- a/x-pack/plugins/spaces/public/management/components/solution_view/solution_view.tsx
+++ b/x-pack/plugins/spaces/public/management/components/solution_view/solution_view.tsx
@@ -7,6 +7,8 @@
import type { EuiSuperSelectOption, EuiThemeComputed } from '@elastic/eui';
import {
+ EuiBetaBadge,
+ EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
@@ -24,6 +26,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Space } from '../../../../common';
+import { SOLUTION_VIEW_CLASSIC } from '../../../../common/constants';
import type { SpaceValidator } from '../../lib';
import { SectionPanel } from '../section_panel';
@@ -40,9 +43,7 @@ const getOptions = ({ size }: EuiThemeComputed): Array
{i18n.translate(
'xpack.spaces.management.manageSpacePage.solutionViewSelect.searchOptionLabel',
- {
- defaultMessage: 'Search',
- }
+ { defaultMessage: 'Search' }
)}
>
),
@@ -55,9 +56,7 @@ const getOptions = ({ size }: EuiThemeComputed): Array
{i18n.translate(
'xpack.spaces.management.manageSpacePage.solutionViewSelect.obltOptionLabel',
- {
- defaultMessage: 'Observability',
- }
+ { defaultMessage: 'Observability' }
)}
>
),
@@ -70,9 +69,7 @@ const getOptions = ({ size }: EuiThemeComputed): Array
{i18n.translate(
'xpack.spaces.management.manageSpacePage.solutionViewSelect.securityOptionLabel',
- {
- defaultMessage: 'Security',
- }
+ { defaultMessage: 'Security' }
)}
>
),
@@ -85,9 +82,7 @@ const getOptions = ({ size }: EuiThemeComputed): Array
{i18n.translate(
'xpack.spaces.management.manageSpacePage.solutionViewSelect.classicOptionLabel',
- {
- defaultMessage: 'Classic',
- }
+ { defaultMessage: 'Classic' }
)}
>
),
@@ -112,25 +107,40 @@ export const SolutionView: FunctionComponent = ({
sectionTitle,
}) => {
const { euiTheme } = useEuiTheme();
+ const showClassicDefaultViewCallout = isEditing && space.solution == null;
return (
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -145,20 +155,43 @@ export const SolutionView: FunctionComponent = ({
>
{
onChange({ ...space, solution });
}}
placeholder={i18n.translate(
'xpack.spaces.management.navigation.solutionViewDefaultValue',
- {
- defaultMessage: 'Select view',
- }
+ { defaultMessage: 'Select solution view' }
)}
isInvalid={validator.validateSolutionView(space, isEditing).isInvalid}
/>
+
+ {showClassicDefaultViewCallout && (
+ <>
+
+
+
+
+
+
+ >
+ )}
diff --git a/x-pack/plugins/spaces/public/management/create_space/create_space_page.test.tsx b/x-pack/plugins/spaces/public/management/create_space/create_space_page.test.tsx
index 4c8617ff007b8..14413b0b2f47b 100644
--- a/x-pack/plugins/spaces/public/management/create_space/create_space_page.test.tsx
+++ b/x-pack/plugins/spaces/public/management/create_space/create_space_page.test.tsx
@@ -6,7 +6,6 @@
*/
import type { EuiCheckboxProps } from '@elastic/eui';
-import { EuiButton } from '@elastic/eui';
import { waitFor } from '@testing-library/react';
import type { ReactWrapper } from 'enzyme';
import React from 'react';
@@ -23,7 +22,6 @@ import type { SolutionView, Space } from '../../../common/types/latest';
import { EventTracker } from '../../analytics';
import type { SpacesManager } from '../../spaces_manager';
import { spacesManagerMock } from '../../spaces_manager/mocks';
-import { ConfirmAlterActiveSpaceModal } from '../components/confirm_alter_active_space_modal';
import { EnabledFeatures } from '../components/enabled_features';
// To be resolved by EUI team.
@@ -153,8 +151,8 @@ describe('ManageSpacePage', () => {
expect(errors).toEqual([
'Enter a name.',
'Enter a URL identifier.',
+ 'Select a solution.',
'Enter initials.',
- 'Select one solution.',
]);
expect(spacesManager.createSpace).not.toHaveBeenCalled();
@@ -168,7 +166,7 @@ describe('ManageSpacePage', () => {
{
const errors = wrapper.find('div.euiFormErrorText').map((node) => node.text());
- expect(errors).toEqual(['Select one solution.']); // requires solution view to be set
+ expect(errors).toEqual(['Select a solution.']); // requires solution view to be set
}
updateSpace(wrapper, false, 'oblt');
@@ -274,7 +272,13 @@ describe('ManageSpacePage', () => {
expect(wrapper.find('input[name="name"]')).toHaveLength(1);
});
- expect(wrapper.find(EnabledFeatures)).toHaveLength(1);
+ // expect visible features table to exist after setting the Solution View to Classic
+ await waitFor(() => {
+ // switch to classic
+ updateSpace(wrapper, false, 'classic');
+ // expect visible features table to exist again
+ expect(wrapper.find(EnabledFeatures)).toHaveLength(1);
+ });
});
it('hides feature visibility controls when not allowed', async () => {
@@ -333,9 +337,6 @@ describe('ManageSpacePage', () => {
await Promise.resolve();
wrapper.update();
-
- // default for create space: expect visible features table to exist
- expect(wrapper.find(EnabledFeatures)).toHaveLength(1);
});
await waitFor(() => {
@@ -353,147 +354,6 @@ describe('ManageSpacePage', () => {
});
});
- it('allows a space to be updated', async () => {
- const spaceToUpdate = {
- id: 'existing-space',
- name: 'Existing Space',
- description: 'hey an existing space',
- color: '#aabbcc',
- initials: 'AB',
- disabledFeatures: [],
- solution: 'es',
- };
-
- const spacesManager = spacesManagerMock.create();
- spacesManager.getSpace = jest.fn().mockResolvedValue({
- ...spaceToUpdate,
- });
- spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
-
- const onLoadSpace = jest.fn();
-
- const wrapper = mountWithIntl(
-
- );
-
- await waitFor(() => {
- wrapper.update();
- expect(spacesManager.getSpace).toHaveBeenCalledWith('existing-space');
- });
-
- expect(onLoadSpace).toHaveBeenCalledWith({
- ...spaceToUpdate,
- });
-
- await Promise.resolve();
-
- wrapper.update();
-
- updateSpace(wrapper, true, 'oblt');
-
- await clickSaveButton(wrapper);
-
- expect(spacesManager.updateSpace).toHaveBeenCalledWith({
- id: 'existing-space',
- name: 'New Space Name',
- description: 'some description',
- color: '#AABBCC',
- initials: 'AB',
- imageUrl: '',
- disabledFeatures: ['feature-1'],
- solution: 'oblt', // solution has been changed
- });
-
- expect(reportEvent).toHaveBeenCalledWith('space_solution_changed', {
- action: 'edit',
- solution: 'oblt',
- solution_prev: 'es',
- space_id: 'existing-space',
- });
- });
-
- it('sets calculated fields for existing spaces', async () => {
- // The Spaces plugin provides functions to calculate the initials and color of a space if they have not been customized. The new space
- // management page explicitly sets these fields when a new space is created, but it should also handle existing "legacy" spaces that do
- // not already have these fields set.
- const spaceToUpdate = {
- id: 'existing-space',
- name: 'Existing Space',
- description: 'hey an existing space',
- color: undefined,
- initials: undefined,
- imageUrl: undefined,
- disabledFeatures: [],
- };
-
- const spacesManager = spacesManagerMock.create();
- spacesManager.getSpace = jest.fn().mockResolvedValue({
- ...spaceToUpdate,
- });
- spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
-
- const onLoadSpace = jest.fn();
-
- const wrapper = mountWithIntl(
-
- );
-
- await waitFor(() => {
- wrapper.update();
- expect(spacesManager.getSpace).toHaveBeenCalledWith('existing-space');
- });
-
- expect(onLoadSpace).toHaveBeenCalledWith({
- ...spaceToUpdate,
- });
-
- await Promise.resolve();
-
- wrapper.update();
-
- // not changing anything, just clicking the "Update space" button
- await clickSaveButton(wrapper);
-
- expect(spacesManager.updateSpace).toHaveBeenCalledWith({
- ...spaceToUpdate,
- color: '#E7664C',
- initials: 'ES',
- imageUrl: '',
- });
- });
-
it('notifies when there is an error retrieving features', async () => {
const spacesManager = spacesManagerMock.create();
spacesManager.createSpace = jest.fn(spacesManager.createSpace);
@@ -528,119 +388,6 @@ describe('ManageSpacePage', () => {
});
});
});
-
- it('warns when updating features in the active space', async () => {
- const spacesManager = spacesManagerMock.create();
- spacesManager.getSpace = jest.fn().mockResolvedValue({
- id: 'my-space',
- name: 'Existing Space',
- description: 'hey an existing space',
- color: '#aabbcc',
- initials: 'AB',
- disabledFeatures: [],
- });
- spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
-
- const wrapper = mountWithIntl(
-
- );
-
- await waitFor(() => {
- wrapper.update();
- expect(spacesManager.getSpace).toHaveBeenCalledWith('my-space');
- });
-
- await Promise.resolve();
-
- wrapper.update();
-
- updateSpace(wrapper);
-
- await clickSaveButton(wrapper);
-
- const warningDialog = wrapper.find(ConfirmAlterActiveSpaceModal);
- expect(warningDialog).toHaveLength(1);
-
- expect(spacesManager.updateSpace).toHaveBeenCalledTimes(0);
-
- const confirmButton = warningDialog
- .find(EuiButton)
- .find('[data-test-subj="confirmModalConfirmButton"]')
- .find('button');
-
- confirmButton.simulate('click');
-
- await Promise.resolve();
-
- wrapper.update();
-
- expect(spacesManager.updateSpace).toHaveBeenCalledTimes(1);
- });
-
- it('does not warn when features are left alone in the active space', async () => {
- const spacesManager = spacesManagerMock.create();
- spacesManager.getSpace = jest.fn().mockResolvedValue({
- id: 'my-space',
- name: 'Existing Space',
- description: 'hey an existing space',
- color: '#aabbcc',
- initials: 'AB',
- disabledFeatures: [],
- });
- spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
-
- const wrapper = mountWithIntl(
-
- );
-
- await waitFor(() => {
- wrapper.update();
- expect(spacesManager.getSpace).toHaveBeenCalledWith('my-space');
- });
-
- await Promise.resolve();
-
- wrapper.update();
-
- updateSpace(wrapper, false);
-
- await clickSaveButton(wrapper);
-
- const warningDialog = wrapper.find(ConfirmAlterActiveSpaceModal);
- expect(warningDialog).toHaveLength(0);
-
- expect(spacesManager.updateSpace).toHaveBeenCalledTimes(1);
- });
});
function updateSpace(
@@ -680,15 +427,6 @@ function toggleFeature(wrapper: ReactWrapper) {
wrapper.update();
}
-async function clickSaveButton(wrapper: ReactWrapper) {
- const saveButton = wrapper.find('button[data-test-subj="save-space-button"]');
- saveButton.simulate('click');
-
- await Promise.resolve();
-
- wrapper.update();
-}
-
function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
diff --git a/x-pack/plugins/spaces/public/management/create_space/create_space_page.tsx b/x-pack/plugins/spaces/public/management/create_space/create_space_page.tsx
index e8204a53fe345..31a5bf885e785 100644
--- a/x-pack/plugins/spaces/public/management/create_space/create_space_page.tsx
+++ b/x-pack/plugins/spaces/public/management/create_space/create_space_page.tsx
@@ -27,15 +27,15 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Space } from '../../../common';
-import { isReservedSpace } from '../../../common';
+import { SOLUTION_VIEW_CLASSIC } from '../../../common/constants';
import type { EventTracker } from '../../analytics';
import { getSpacesFeatureDescription } from '../../constants';
import { getSpaceColor, getSpaceInitials } from '../../space_avatar';
import type { SpacesManager } from '../../spaces_manager';
import { UnauthorizedPrompt } from '../components';
import { ConfirmAlterActiveSpaceModal } from '../components/confirm_alter_active_space_modal';
+import { CustomizeAvatar } from '../components/customize_avatar';
import { CustomizeSpace } from '../components/customize_space';
-import { DeleteSpacesButton } from '../components/delete_spaces_button';
import { EnabledFeatures } from '../components/enabled_features';
import { SolutionView } from '../components/solution_view';
import { toSpaceIdentifier } from '../lib';
@@ -60,7 +60,6 @@ interface State {
features: KibanaFeature[];
originalSpace?: Partial;
showAlteringActiveSpaceDialog: boolean;
- showVisibleFeaturesPicker: boolean;
haveDisabledFeaturesChanged: boolean;
hasSolutionViewChanged: boolean;
isLoading: boolean;
@@ -80,7 +79,6 @@ export class CreateSpacePage extends Component {
this.state = {
isLoading: true,
showAlteringActiveSpaceDialog: false,
- showVisibleFeaturesPicker: !!props.allowFeatureVisibility,
saveInProgress: false,
space: {
color: getSpaceColor({}),
@@ -185,12 +183,9 @@ export class CreateSpacePage extends Component {
return (
@@ -201,25 +196,30 @@ export class CreateSpacePage extends Component
{
space={this.state.space}
onChange={this.onSolutionViewChange}
validator={this.validator}
- isEditing={this.editingExistingSpace()}
- sectionTitle={i18n.translate(
- 'xpack.spaces.management.manageSpacePage.navigationTitle',
- { defaultMessage: 'Navigation' }
- )}
+ isEditing={false}
/>
>
)}
- {this.state.showVisibleFeaturesPicker && (
- <>
-
-
- >
- )}
+ {this.props.allowFeatureVisibility &&
+ (!this.state.space.solution || this.state.space.solution === SOLUTION_VIEW_CLASSIC) && (
+ <>
+
+
+ >
+ )}
+
+
+
+
@@ -240,14 +240,6 @@ export class CreateSpacePage extends Component {
};
public getTitle = () => {
- if (this.editingExistingSpace()) {
- return (
-
- );
- }
return (
{
};
public getChangeImpactWarning = () => {
- if (!this.editingExistingSpace()) return null;
const { haveDisabledFeaturesChanged, hasSolutionViewChanged } = this.state;
if (!haveDisabledFeaturesChanged && !hasSolutionViewChanged) return null;
@@ -289,13 +280,6 @@ export class CreateSpacePage extends Component {
}
);
- const updateSpaceText = i18n.translate(
- 'xpack.spaces.management.manageSpacePage.updateSpaceButton',
- {
- defaultMessage: 'Update space',
- }
- );
-
const cancelButtonText = i18n.translate(
'xpack.spaces.management.manageSpacePage.cancelSpaceButton',
{
@@ -303,8 +287,6 @@ export class CreateSpacePage extends Component {
}
);
- const saveText = this.editingExistingSpace() ? updateSpaceText : createSpaceText;
-
return (
@@ -314,7 +296,7 @@ export class CreateSpacePage extends Component {
data-test-subj="save-space-button"
isLoading={this.state.saveInProgress}
>
- {saveText}
+ {createSpaceText}
@@ -323,37 +305,12 @@ export class CreateSpacePage extends Component {
- {this.getActionButton()}
);
};
- public getActionButton = () => {
- if (this.state.space && this.editingExistingSpace() && !isReservedSpace(this.state.space)) {
- return (
-
-
-
- );
- }
-
- return null;
- };
-
private onSolutionViewChange = (space: Partial) => {
- if (this.props.allowFeatureVisibility) {
- let showVisibleFeaturesPicker = false;
- if (space.solution === 'classic' || space.solution == null) {
- showVisibleFeaturesPicker = true;
- }
- this.setState((state) => ({ ...state, showVisibleFeaturesPicker }));
- }
+ this.setState((state) => ({ ...state, solution: space.solution }));
this.onSpaceChange(space);
};
@@ -366,14 +323,8 @@ export class CreateSpacePage extends Component {
public saveSpace = () => {
this.validator.enableValidation();
- const originalSpace: Space = this.state.originalSpace as Space;
const space: Space = this.state.space as Space;
- const { haveDisabledFeaturesChanged, hasSolutionViewChanged } = this.state;
- const result = this.validator.validateForSave(
- space,
- this.editingExistingSpace(),
- this.props.allowSolutionVisibility
- );
+ const result = this.validator.validateForSave(space, false, this.props.allowSolutionVisibility);
if (result.isInvalid) {
this.setState({
formError: result,
@@ -382,24 +333,7 @@ export class CreateSpacePage extends Component {
return;
}
- if (this.editingExistingSpace()) {
- const { spacesManager } = this.props;
-
- spacesManager.getActiveSpace().then((activeSpace) => {
- const editingActiveSpace = activeSpace.id === originalSpace.id;
-
- if (editingActiveSpace && (haveDisabledFeaturesChanged || hasSolutionViewChanged)) {
- this.setState({
- showAlteringActiveSpaceDialog: true,
- });
-
- return;
- }
- this.performSave();
- });
- } else {
- this.performSave();
- }
+ this.performSave();
};
private loadSpace = async (spaceId: string, featuresPromise: Promise) => {
@@ -472,15 +406,8 @@ export class CreateSpacePage extends Component {
solution,
};
- let action;
- const isEditing = this.editingExistingSpace();
const { spacesManager, eventTracker } = this.props;
-
- if (isEditing) {
- action = spacesManager.updateSpace(params);
- } else {
- action = spacesManager.createSpace(params);
- }
+ const action = spacesManager.createSpace(params);
this.setState({ saveInProgress: true });
@@ -493,7 +420,7 @@ export class CreateSpacePage extends Component {
spaceId: id,
solution,
solutionPrev: this.state.originalSpace?.solution,
- action: isEditing ? 'edit' : 'create',
+ action: 'create',
});
};
@@ -536,6 +463,4 @@ export class CreateSpacePage extends Component {
};
private backToSpacesList = () => this.props.history.push('/');
-
- private editingExistingSpace = () => !!this.props.spaceId;
}
diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space.tsx
index cd2bd76a57928..825bc6977ad1c 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/edit_space.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space.tsx
@@ -201,8 +201,8 @@ export const EditSpace: FC = ({
-
-
+
+
diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_features_tab.tsx
deleted file mode 100644
index f5bfbe79ec2d4..0000000000000
--- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_features_tab.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
-import type { FC } from 'react';
-import React from 'react';
-
-import type { KibanaFeature } from '@kbn/features-plugin/common';
-import { FormattedMessage } from '@kbn/i18n-react';
-
-import { useEditSpaceServices } from './provider';
-import type { Space } from '../../../common';
-import { FeatureTable } from '../components/enabled_features/feature_table';
-import { SectionPanel } from '../components/section_panel';
-
-interface Props {
- space: Partial;
- features: KibanaFeature[];
- onChange: (updatedSpace: Partial) => void;
-}
-
-export const EditSpaceEnabledFeatures: FC = ({ features, space, onChange }) => {
- const { capabilities, getUrlForApp } = useEditSpaceServices();
- const canManageRoles = capabilities.roles?.save === true;
-
- if (!features) {
- return null;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ) : (
-
- ),
- }}
- />
-
-
-
-
-
-
-
-
- );
-};
diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.tsx
index 24269528916f8..cb82b5f800fbb 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.tsx
@@ -13,7 +13,6 @@ import type { KibanaFeature } from '@kbn/features-plugin/common';
import { i18n } from '@kbn/i18n';
import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt';
-import { EditSpaceEnabledFeatures } from './edit_space_features_tab';
import { EditSpaceTabFooter } from './footer';
import { useEditSpaceServices } from './provider';
import type { Space } from '../../../common';
@@ -21,7 +20,9 @@ import { SOLUTION_VIEW_CLASSIC } from '../../../common/constants';
import { getSpaceInitials } from '../../space_avatar';
import { ConfirmDeleteModal } from '../components';
import { ConfirmAlterActiveSpaceModal } from '../components/confirm_alter_active_space_modal';
+import { CustomizeAvatar } from '../components/customize_avatar';
import { CustomizeSpace } from '../components/customize_space';
+import { EnabledFeatures } from '../components/enabled_features';
import { SolutionView } from '../components/solution_view';
import { SpaceValidator } from '../lib';
import type { CustomizeSpaceFormValues } from '../types';
@@ -249,17 +250,15 @@ export const EditSpaceSettingsTab: React.FC = ({ space, features, history
- {i18n.translate(
+ iconType="iInCircle"
+ title={i18n.translate(
'xpack.spaces.management.spaceDetails.spaceChangesWarning.impactAllUsersInSpace',
{
- defaultMessage: 'The changes made will impact all users in the space.',
+ defaultMessage: 'The changes will apply to all users of the space.',
}
)}
-
+ data-test-subj="space-edit-page-user-impact-warning"
+ />
>
)
);
@@ -289,10 +288,10 @@ export const EditSpaceSettingsTab: React.FC = ({ space, features, history
>
)}
- {props.allowFeatureVisibility && (solution == null || solution === SOLUTION_VIEW_CLASSIC) && (
+ {props.allowFeatureVisibility && (!solution || solution === SOLUTION_VIEW_CLASSIC) && (
<>
- = ({ space, features, history
>
)}
+
+
+
+
{doShowUserImpactWarning()}
diff --git a/x-pack/plugins/spaces/public/management/edit_space/footer.tsx b/x-pack/plugins/spaces/public/management/edit_space/footer.tsx
index 013a356f9b400..b00494cbee51c 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/footer.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/footer.tsx
@@ -31,57 +31,57 @@ export const EditSpaceTabFooter: React.FC = ({
onClickSubmit,
onClickDeleteSpace,
}) => {
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
return (
- <>
- {isLoading && (
-
-
-
-
-
+
+ {isDirty && (
+
+
+
+
+
)}
- {!isLoading && (
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
- {isDirty && (
-
-
-
-
-
- )}
-
- )}
- >
+
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx
index 658730a848a33..276efb7f92526 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx
@@ -354,7 +354,7 @@ export const PrivilegesRolesForm: FC = (props) => {
@@ -367,7 +367,8 @@ export const PrivilegesRolesForm: FC = (props) => {
helpText={i18n.translate(
'xpack.spaces.management.spaceDetails.roles.selectRolesHelp',
{
- defaultMessage: 'Select Kibana spaces to which you wish to assign privileges.',
+ defaultMessage:
+ 'Users assigned to selected roles will gain access to this space.',
}
)}
>
@@ -380,6 +381,10 @@ export const PrivilegesRolesForm: FC = (props) => {
values: { spaceName: space.name },
}
)}
+ placeholder={i18n.translate(
+ 'xpack.spaces.management.spaceDetails.roles.selectRolesPlaceholder',
+ { defaultMessage: 'Add a role...' }
+ )}
isLoading={fetchingDataDeps}
options={createRolesComboBoxOptions(spaceUnallocatedRoles)}
selectedOptions={selectedRoles}
diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assigned_roles_table.tsx
index 6a1d9f24bc042..ffe7ecba85ec0 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assigned_roles_table.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assigned_roles_table.tsx
@@ -78,6 +78,7 @@ const getTableColumns = ({
name: i18n.translate('xpack.spaces.management.spaceDetails.rolesTable.column.name.title', {
defaultMessage: 'Role',
}),
+ width: '45%',
},
{
field: 'privileges',
@@ -118,25 +119,25 @@ const getTableColumns = ({
{ defaultMessage: 'Role type' }
),
render: (_value: Role['metadata']) => {
- return React.createElement(EuiBadge, {
- children: _value?._reserved
- ? i18n.translate(
+ return _value?._reserved
+ ? React.createElement(EuiBadge, {
+ children: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.reserved',
{ defaultMessage: 'Reserved' }
- )
- : i18n.translate(
- 'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.custom',
- { defaultMessage: 'Custom' }
),
- color: _value?._reserved ? undefined : 'success',
- });
+ color: 'primary',
+ })
+ : null;
},
},
];
if (!isReadOnly) {
columns.push({
- name: 'Actions',
+ name: i18n.translate(
+ 'xpack.spaces.management.spaceDetails.rolesTable.column.actions.columnHeaderName',
+ { defaultMessage: 'Actions' }
+ ),
actions: [
{
type: 'icon',
@@ -163,22 +164,22 @@ const getTableColumns = ({
: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.actions.notEditableDescription.isAssignedToAll',
{
- defaultMessage: `Can't perform actions on a role that is assigned to all spaces`,
+ defaultMessage: `You can't edit the access of a role that is assigned to all spaces.`,
}
),
- isPrimary: true,
+ showOnHover: true,
enabled: () => false,
available: (rowRecord) => !isEditableRole(rowRecord),
},
{
type: 'icon',
icon: 'pencil',
+ isPrimary: true,
'data-test-subj': 'spaceRoleCellEditAction',
name: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.actions.edit.title',
{ defaultMessage: 'Remove from space' }
),
- isPrimary: true,
description: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.actions.edit.description',
{
@@ -186,15 +187,14 @@ const getTableColumns = ({
'Click this action to edit the role privileges of this user for this space.',
}
),
- showOnHover: true,
available: (rowRecord) => isEditableRole(rowRecord),
onClick: onClickRowEditAction,
},
{
- isPrimary: true,
type: 'icon',
icon: 'trash',
color: 'danger',
+ isPrimary: true,
'data-test-subj': 'spaceRoleCellDeleteAction',
name: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.actions.remove.title',
@@ -204,7 +204,6 @@ const getTableColumns = ({
'xpack.spaces.management.spaceDetails.rolesTable.column.actions.edit.description',
{ defaultMessage: 'Click this action to remove the user from this space.' }
),
- showOnHover: true,
available: (rowRecord) => isEditableRole(rowRecord),
onClick: onClickRowRemoveAction,
},
diff --git a/x-pack/plugins/spaces/public/management/lib/validate_space.ts b/x-pack/plugins/spaces/public/management/lib/validate_space.ts
index 7a7980028dad0..89205cc91f2ef 100644
--- a/x-pack/plugins/spaces/public/management/lib/validate_space.ts
+++ b/x-pack/plugins/spaces/public/management/lib/validate_space.ts
@@ -181,7 +181,7 @@ export class SpaceValidator {
if (!space.solution) {
return invalid(
i18n.translate('xpack.spaces.management.validateSpace.requiredSolutionViewErrorMessage', {
- defaultMessage: 'Select one solution.',
+ defaultMessage: 'Select a solution.',
})
);
}
diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx
index 091057a2f4a4c..c8707f8959f0c 100644
--- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx
+++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx
@@ -95,7 +95,7 @@ describe('SpacesGridPage', () => {
expect(wrapper.find('EuiInMemoryTable').prop('items')).toBe(spaces);
expect(wrapper.find('EuiInMemoryTable').prop('columns')).not.toContainEqual({
field: 'solution',
- name: 'Solution View',
+ name: 'Solution view',
sortable: true,
render: expect.any(Function),
});
@@ -155,7 +155,7 @@ describe('SpacesGridPage', () => {
expect(wrapper.find('EuiInMemoryTable').prop('items')).toBe(spacesWithSolution);
expect(wrapper.find('EuiInMemoryTable').prop('columns')).toContainEqual({
field: 'solution',
- name: 'Solution View',
+ name: 'Solution view',
sortable: true,
render: expect.any(Function),
});
diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx
index 5ac3ecf0ca687..462b65f327ebc 100644
--- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx
+++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx
@@ -10,7 +10,7 @@ import {
type EuiBasicTableColumn,
EuiButton,
EuiCallOut,
- EuiFlexGroup,
+ EuiFlexGrid,
EuiFlexItem,
EuiInMemoryTable,
EuiLink,
@@ -19,6 +19,7 @@ import {
EuiPageSection,
EuiSpacer,
EuiText,
+ useIsWithinBreakpoints,
} from '@elastic/eui';
import React, { Component, lazy, Suspense } from 'react';
@@ -152,9 +153,7 @@ export class SpacesGridPage extends Component {
box: {
placeholder: i18n.translate(
'xpack.spaces.management.spacesGridPage.searchPlaceholder',
- {
- defaultMessage: 'Search',
- }
+ { defaultMessage: 'Search' }
),
},
}}
@@ -281,28 +280,49 @@ export class SpacesGridPage extends Component {
defaultMessage: 'Space',
}),
sortable: true,
- render: (value: string, rowRecord: Space) => (
-
-
- {
+ const SpaceName = () => {
+ const isCurrent = this.state.activeSpace?.id === rowRecord.id;
+ const isWide = useIsWithinBreakpoints(['xl']);
+ const gridColumns = isCurrent && isWide ? 2 : 1;
+ return (
+
- {value}
-
-
- {this.state.activeSpace?.id === rowRecord.id && (
-
-
- {i18n.translate('xpack.spaces.management.spacesGridPage.currentSpaceMarkerText', {
- defaultMessage: 'current',
- })}
-
-
- )}
-
- ),
+
+
+ {value}
+
+
+
+ {isCurrent && (
+
+
+ {i18n.translate(
+ 'xpack.spaces.management.spacesGridPage.currentSpaceMarkerText',
+ { defaultMessage: 'current' }
+ )}
+
+
+ )}
+
+
+ );
+ };
+
+ return ;
+ },
'data-test-subj': 'spacesListTableRowNameCell',
+ width: '15%',
},
{
field: 'description',
@@ -311,7 +331,7 @@ export class SpacesGridPage extends Component {
}),
sortable: true,
truncateText: true,
- width: '30%',
+ width: '45%',
},
];
@@ -331,7 +351,7 @@ export class SpacesGridPage extends Component {
return (
);
}
@@ -377,7 +397,7 @@ export class SpacesGridPage extends Component {
config.push({
field: 'solution',
name: i18n.translate('xpack.spaces.management.spacesGridPage.solutionColumnName', {
- defaultMessage: 'Solution View',
+ defaultMessage: 'Solution view',
}),
sortable: true,
render: (solution: Space['solution'], record: Space) => (
diff --git a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.scss b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.scss
index f444b45192f8b..8e14b2876f429 100644
--- a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.scss
+++ b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.scss
@@ -3,8 +3,9 @@
}
.spcMenu__spacesList {
- @include euiYScrollWithShadows;
max-height: $euiSizeXL * 10;
+
+ @include euiYScrollWithShadows;
}
.spcMenu__searchFieldWrapper {
diff --git a/x-pack/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap b/x-pack/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap
index 8dfe89ef172d4..4419707ab45f4 100644
--- a/x-pack/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap
+++ b/x-pack/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap
@@ -8,7 +8,7 @@ exports[`it renders with custom logo 1`] = `
>
@@ -69,7 +69,7 @@ exports[`it renders without crashing 1`] = `
>
diff --git a/x-pack/plugins/spaces/public/space_selector/space_selector.scss b/x-pack/plugins/spaces/public/space_selector/space_selector.scss
index 4629e92a201f5..f589347e592ed 100644
--- a/x-pack/plugins/spaces/public/space_selector/space_selector.scss
+++ b/x-pack/plugins/spaces/public/space_selector/space_selector.scss
@@ -4,6 +4,9 @@
.spcSelectorBackground {
@include kibanaFullScreenGraphics;
+}
+
+.spcSelectorBackground__nonMixinAttributes {
z-index: -1;
pointer-events: none;
}
diff --git a/x-pack/plugins/spaces/public/space_selector/space_selector.tsx b/x-pack/plugins/spaces/public/space_selector/space_selector.tsx
index e90d1c4a10954..3d72392552e38 100644
--- a/x-pack/plugins/spaces/public/space_selector/space_selector.tsx
+++ b/x-pack/plugins/spaces/public/space_selector/space_selector.tsx
@@ -124,7 +124,10 @@ export class SpaceSelector extends Component {
>
{/* Portal the fixed background graphic so it doesn't affect page positioning or overlap on top of global banners */}
-
+
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/crowdstrike/crowdstrike_connector.tsx b/x-pack/plugins/stack_connectors/public/connector_types/crowdstrike/crowdstrike_connector.tsx
index 3af2cd8c4648a..b468cd3bbb712 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/crowdstrike/crowdstrike_connector.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/crowdstrike/crowdstrike_connector.tsx
@@ -15,13 +15,11 @@ import {
} from '@kbn/triggers-actions-ui-plugin/public';
import * as i18n from './translations';
-const CROWDSTRIKE_DEFAULT_API_URL = 'https://api.crowdstrike.com';
const configFormSchema: ConfigFieldSchema[] = [
{
id: 'url',
label: i18n.URL_LABEL,
isUrlField: true,
- defaultValue: CROWDSTRIKE_DEFAULT_API_URL,
},
];
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/crowdstrike/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/crowdstrike/translations.ts
index 1fb91cecfe992..e10226f532914 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/crowdstrike/translations.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/crowdstrike/translations.ts
@@ -19,14 +19,14 @@ export const URL_LABEL = i18n.translate(
export const CLIENT_ID_LABEL = i18n.translate(
'xpack.stackConnectors.security.crowdstrike.config.clientIdTextFieldLabel',
{
- defaultMessage: 'Crowdstrike client ID',
+ defaultMessage: 'Crowdstrike Client ID',
}
);
export const CLIENT_SECRET_LABEL = i18n.translate(
'xpack.stackConnectors.security.crowdstrike.config.clientSecretTextFieldLabel',
{
- defaultMessage: 'Client secret',
+ defaultMessage: 'Client Secret',
}
);
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 6e6b150b41c0a..178c036061fdd 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -35248,7 +35248,6 @@
"xpack.securitySolution.appLinks.network.flows": "Flux",
"xpack.securitySolution.appLinks.network.http": "HTTP",
"xpack.securitySolution.appLinks.network.tls": "TLS",
- "xpack.securitySolution.appLinks.notesManagementDescription": "Visualisez et supprimez des notes.",
"xpack.securitySolution.appLinks.overview": "Aperçu",
"xpack.securitySolution.appLinks.overviewDescription": "Résumé de votre activité d'environnement de sécurité, y compris les alertes, les événements, les éléments récents et un fil d'actualités !",
"xpack.securitySolution.appLinks.policiesDescription": "Utilisez les politiques pour personnaliser les protections des points de terminaison et de charge de travail cloud, et d'autres configurations.",
@@ -37770,7 +37769,6 @@
"xpack.securitySolution.endpoint.list.loadingPolicies": "Chargement des intégrations",
"xpack.securitySolution.endpoint.list.noEndpointsInstructions": "Vous avez ajouté l'intégration Elastic Defend. Vous pouvez maintenant enregistrer vos agents en suivant la procédure ci-dessous.",
"xpack.securitySolution.endpoint.list.noEndpointsPrompt": "Étape suivante : Enregistrer un agent avec Elastic Defend",
- "xpack.securitySolution.endpoint.list.noPolicies": "Il n'existe aucune intégration.",
"xpack.securitySolution.endpoint.list.os": "Système d'exploitation",
"xpack.securitySolution.endpoint.list.pageSubTitle": "Hôtes exécutant Elastic Defend",
"xpack.securitySolution.endpoint.list.pageTitle": "Points de terminaison",
@@ -39504,7 +39502,6 @@
"xpack.securitySolution.navigation.manage": "Gérer",
"xpack.securitySolution.navigation.network": "Réseau",
"xpack.securitySolution.navigation.newRuleTitle": "Créer une nouvelle règle",
- "xpack.securitySolution.navigation.notesManagement": "Notes",
"xpack.securitySolution.navigation.overview": "Aperçu",
"xpack.securitySolution.navigation.protectionUpdates": "Mises à jour de la protection",
"xpack.securitySolution.navigation.responseActionsHistory": "Historique des actions de réponse",
@@ -42708,8 +42705,6 @@
"xpack.spaces.management.deselectAllFeaturesLink": "Tout masquer",
"xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "Touche bascule de catégorie",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "Définir la visibilité des fonctionnalités",
- "xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText": "gérer les rôles de sécurité",
- "xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "Les fonctionnalités masquées sont supprimées de l'interface utilisateur, mais pas désactivées. Pour sécuriser l'accès aux fonctionnalités, {manageRolesLink}.",
"xpack.spaces.management.featureAccordionSwitchLabel": "{enabledCount} fonctionnalités visibles / {featureCount}",
"xpack.spaces.management.featureVisibilityTitle": "Visibilité des fonctionnalités",
"xpack.spaces.management.hideAllFeaturesText": "Tout masquer",
@@ -42721,15 +42716,11 @@
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "Créer l'espace",
"xpack.spaces.management.manageSpacePage.describeSpaceDescription": "Attribuez à votre espace un nom facile à retenir.",
"xpack.spaces.management.manageSpacePage.describeSpaceTitle": "Décrire cet espace",
- "xpack.spaces.management.manageSpacePage.editSpaceTitle": "Modifier l'espace",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "Erreur lors du chargement de l'espace : {message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "Erreur lors de l'enregistrement de l'espace : {message}",
- "xpack.spaces.management.manageSpacePage.featuresTitle": "Fonctionnalités",
- "xpack.spaces.management.manageSpacePage.generalTitle": "Général",
"xpack.spaces.management.manageSpacePage.loadErrorTitle": "Erreur lors du chargement des fonctionnalités disponibles",
"xpack.spaces.management.manageSpacePage.loadingMessage": "Chargement…",
"xpack.spaces.management.manageSpacePage.nameFormRowLabel": "Nom",
- "xpack.spaces.management.manageSpacePage.navigationTitle": "Navigation",
"xpack.spaces.management.manageSpacePage.optionalLabel": "Facultatif",
"xpack.spaces.management.manageSpacePage.setSolutionViewDescription": "Détermine la navigation que tous les utilisateurs verront pour cet espace. Chaque vue de solution contient des fonctionnalités de Outils d'analyse et de Gestion.",
"xpack.spaces.management.manageSpacePage.setSolutionViewMessage": "Définir la vue de la solution",
@@ -42740,7 +42731,6 @@
"xpack.spaces.management.manageSpacePage.spaceDescriptionFormRowLabel": "Description",
"xpack.spaces.management.manageSpacePage.spaceDescriptionHelpText": "La description s'affiche sur l'écran de sélection de l'espace.",
"xpack.spaces.management.manageSpacePage.spaceSuccessfullySavedNotificationMessage": "L'espace {name} a été enregistré.",
- "xpack.spaces.management.manageSpacePage.updateSpaceButton": "Mettre à jour l'espace",
"xpack.spaces.management.navigation.solutionViewLabel": "Afficher la solution",
"xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "Les espaces réservés sont intégrés et ne peuvent être que partiellement modifiés.",
"xpack.spaces.management.selectAllFeaturesLink": "Afficher tout",
@@ -43883,11 +43873,9 @@
"xpack.synthetics.alertRules.monitorStatus.actionVariables.state.status": "Statut du moniteur (par ex. \"arrêté\").",
"xpack.synthetics.alertRules.monitorStatus.browser.label": "navigateur",
"xpack.synthetics.alertRules.monitorStatus.host.label": "Hôte",
- "xpack.synthetics.alertRules.monitorStatus.reasonMessage": "Le moniteur \"{name}\" de {location} est {status}. Vérifié à {checkedAt}.",
"xpack.synthetics.alertRules.monitorStatus.unavailableUrlLabel": "(indisponible)",
"xpack.synthetics.alerts.monitorStatus.absoluteLink.label": "- Lien",
"xpack.synthetics.alerts.monitorStatus.defaultRecovery.status": "a récupéré",
- "xpack.synthetics.alerts.monitorStatus.deleteMonitor.reason": "le moniteur a été supprimé",
"xpack.synthetics.alerts.monitorStatus.deleteMonitor.status": "a été supprimé",
"xpack.synthetics.alerts.monitorStatus.downLabel": "bas",
"xpack.synthetics.alerts.monitorStatus.relativeLink.label": "- Lien relatif",
@@ -43897,7 +43885,6 @@
"xpack.synthetics.alerts.monitorStatus.upCheck.status": "est désormais disponible",
"xpack.synthetics.alerts.settings.addConnector": "Ajouter un connecteur",
"xpack.synthetics.alerts.syntheticsMonitorStatus.clientName": "Statut du moniteur",
- "xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoverySubjectMessage": "\"{monitorName}\" ({locationName}) {recoveryStatus} - Elastic Synthetics",
"xpack.synthetics.alerts.syntheticsMonitorStatus.description": "Alerte lorsqu'un moniteur est arrêté.",
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultRecoverySubjectMessage": "L'alerte a été résolue pour le certificat {commonName} - Elastic Synthetics",
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultSubjectMessage": "L'alerte a été déclenchée pour le certificat {commonName} - Elastic Synthetics",
@@ -47697,4 +47684,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "Ce champ est requis.",
"xpack.watcher.watcherDescription": "Détectez les modifications survenant dans vos données en créant, gérant et monitorant des alertes."
}
-}
+}
\ No newline at end of file
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 337d7ce48044d..e577a0253528d 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -34992,7 +34992,6 @@
"xpack.securitySolution.appLinks.network.flows": "Flow",
"xpack.securitySolution.appLinks.network.http": "HTTP",
"xpack.securitySolution.appLinks.network.tls": "TLS",
- "xpack.securitySolution.appLinks.notesManagementDescription": "メモを可視化して、削除します。",
"xpack.securitySolution.appLinks.overview": "概要",
"xpack.securitySolution.appLinks.overviewDescription": "アラート、イベント、最近のアイテム、ニュースフィードを含む、セキュリティ環境アクティビティの概要。",
"xpack.securitySolution.appLinks.policiesDescription": "ポリシーを使用して、エンドポイントおよびクラウドワークロード保護、ならびに他の構成をカスタマイズします。",
@@ -37512,7 +37511,6 @@
"xpack.securitySolution.endpoint.list.loadingPolicies": "統合を読み込んでいます",
"xpack.securitySolution.endpoint.list.noEndpointsInstructions": "Elastic Defend統合を追加しました。次の手順を使用して、エージェントを登録してください。",
"xpack.securitySolution.endpoint.list.noEndpointsPrompt": "次のステップ:Elastic Defendにエージェントを登録する",
- "xpack.securitySolution.endpoint.list.noPolicies": "統合はありません。",
"xpack.securitySolution.endpoint.list.os": "OS",
"xpack.securitySolution.endpoint.list.pageSubTitle": "Elastic Defendを実行しているホスト",
"xpack.securitySolution.endpoint.list.pageTitle": "エンドポイント",
@@ -39247,7 +39245,6 @@
"xpack.securitySolution.navigation.manage": "管理",
"xpack.securitySolution.navigation.network": "ネットワーク",
"xpack.securitySolution.navigation.newRuleTitle": "新規ルールを作成",
- "xpack.securitySolution.navigation.notesManagement": "メモ",
"xpack.securitySolution.navigation.overview": "概要",
"xpack.securitySolution.navigation.protectionUpdates": "保護更新",
"xpack.securitySolution.navigation.responseActionsHistory": "対応アクション履歴",
@@ -42448,8 +42445,6 @@
"xpack.spaces.management.deselectAllFeaturesLink": "すべて非表示",
"xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "カテゴリ切り替え",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "機能の表示を設定",
- "xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText": "セキュリティロールを管理",
- "xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "非表示の機能はユーザーインターフェイスから削除されますが、無効にされません。機能へのアクセスを保護するには、{manageRolesLink}してください。",
"xpack.spaces.management.featureAccordionSwitchLabel": "{featureCount} 件中 {enabledCount} 件の機能を表示中",
"xpack.spaces.management.featureVisibilityTitle": "機能の表示",
"xpack.spaces.management.hideAllFeaturesText": "すべて非表示",
@@ -42461,15 +42456,11 @@
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "スペースを作成",
"xpack.spaces.management.manageSpacePage.describeSpaceDescription": "スペースに覚えやすい名前を付けます。",
"xpack.spaces.management.manageSpacePage.describeSpaceTitle": "このスペースを説明",
- "xpack.spaces.management.manageSpacePage.editSpaceTitle": "スペースの編集",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "スペースの読み込み中にエラーが発生:{message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "スペースの保存中にエラーが発生:{message}",
- "xpack.spaces.management.manageSpacePage.featuresTitle": "機能",
- "xpack.spaces.management.manageSpacePage.generalTitle": "一般",
"xpack.spaces.management.manageSpacePage.loadErrorTitle": "利用可能な機能の読み込みエラー",
"xpack.spaces.management.manageSpacePage.loadingMessage": "読み込み中…",
"xpack.spaces.management.manageSpacePage.nameFormRowLabel": "名前",
- "xpack.spaces.management.manageSpacePage.navigationTitle": "ナビゲーション",
"xpack.spaces.management.manageSpacePage.optionalLabel": "オプション",
"xpack.spaces.management.manageSpacePage.setSolutionViewDescription": "すべてのユーザーにこのスペースで表示されるナビゲーションを決定します。各ソリューションビューには、分析ツールと管理の機能が含まれます。",
"xpack.spaces.management.manageSpacePage.setSolutionViewMessage": "ソリューションビューを設定",
@@ -42480,7 +42471,6 @@
"xpack.spaces.management.manageSpacePage.spaceDescriptionFormRowLabel": "説明",
"xpack.spaces.management.manageSpacePage.spaceDescriptionHelpText": "説明はスペース選択画面に表示されます。",
"xpack.spaces.management.manageSpacePage.spaceSuccessfullySavedNotificationMessage": "スペース {name} が保存されました。",
- "xpack.spaces.management.manageSpacePage.updateSpaceButton": "スペースを更新",
"xpack.spaces.management.navigation.solutionViewLabel": "ソリューションビュー",
"xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "リザーブされたスペースはビルトインのため、部分的な変更しかできません。",
"xpack.spaces.management.selectAllFeaturesLink": "すべて表示",
@@ -43622,11 +43612,9 @@
"xpack.synthetics.alertRules.monitorStatus.actionVariables.state.status": "監視ステータス(例:「ダウン」)。",
"xpack.synthetics.alertRules.monitorStatus.browser.label": "ブラウザー",
"xpack.synthetics.alertRules.monitorStatus.host.label": "ホスト",
- "xpack.synthetics.alertRules.monitorStatus.reasonMessage": "{location}のモニター\"{name}\"は{status}です。{checkedAt}に確認されました。",
"xpack.synthetics.alertRules.monitorStatus.unavailableUrlLabel": "(使用不可)",
"xpack.synthetics.alerts.monitorStatus.absoluteLink.label": "- リンク",
"xpack.synthetics.alerts.monitorStatus.defaultRecovery.status": "回復しました",
- "xpack.synthetics.alerts.monitorStatus.deleteMonitor.reason": "モニターが削除されました",
"xpack.synthetics.alerts.monitorStatus.deleteMonitor.status": "が削除されました",
"xpack.synthetics.alerts.monitorStatus.downLabel": "ダウン",
"xpack.synthetics.alerts.monitorStatus.relativeLink.label": "- 相対リンク",
@@ -43636,10 +43624,6 @@
"xpack.synthetics.alerts.monitorStatus.upCheck.status": "現在起動しています",
"xpack.synthetics.alerts.settings.addConnector": "コネクターの追加",
"xpack.synthetics.alerts.syntheticsMonitorStatus.clientName": "監視ステータス",
- "xpack.synthetics.alerts.syntheticsMonitorStatus.defaultActionMessage": "{locationName}の\"{monitorName}\"は{status}です - Elastic Synthetics\n\n詳細:\n\n- モニター名:{monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- モニタータイプ:{monitorType} \n- 確認日時:{checkedAt} \n- 開始場所:{locationName} \n- 受信したエラー:{lastErrorMessage} \n{linkMessage}",
- "xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoveryMessage": "{locationName}の\"{monitorName}\"のアラートはアクティブではありません:{recoveryReason} - Elastic Synthetics\n\n詳細:\n\n- モニター名:{monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- モニタータイプ:{monitorType} \n- 開始場所:{locationName} \n- 前回受信したエラー:{lastErrorMessage} \n{linkMessage}",
- "xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoverySubjectMessage": "\"{monitorName}\" ({locationName}) {recoveryStatus} - Elastic Synthetics",
- "xpack.synthetics.alerts.syntheticsMonitorStatus.defaultSubjectMessage": "\"{monitorName}\" ({locationName})は停止しています - Elastic Synthetics",
"xpack.synthetics.alerts.syntheticsMonitorStatus.description": "モニターがダウンしているときにアラートを通知します。",
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultRecoverySubjectMessage": "証明書{commonName}のアラートが解決しました - Elastic Synthetics",
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultSubjectMessage": "証明書{commonName}のアラートがトリガーされました - Elastic Synthetics",
@@ -44481,8 +44465,6 @@
"xpack.synthetics.rules.tls.agingLabel": "古すぎます",
"xpack.synthetics.rules.tls.clientName": "シンセティックTLS",
"xpack.synthetics.rules.tls.criteriaExpression.ariaLabel": "このアラートで監視されているモニターの条件を示す式",
- "xpack.synthetics.rules.tls.defaultActionMessage": "TLS証明書{commonName} {status} - Elastic Synthetics\n\n詳細:\n\n- 概要:{summary}\n- 共通名:{commonName}\n- 発行元:{issuer}\n- モニター:{monitorName} \n- モニターURL:{monitorUrl} \n- モニタータイプ:{monitorType} \n- 開始場所:{locationName}",
- "xpack.synthetics.rules.tls.defaultRecoveryMessage": "モニター\"{monitorName}\"のTLSアラートが回復しました - Elastic Synthetics\n\n詳細:\n\n- 概要:{summary}\n- 新しいステータス:{newStatus}\n- 前のステータス:{previousStatus}\n- モニター:{monitorName} \n- URL:{monitorUrl} \n- モニタータイプ:{monitorType} \n- 開始場所:{locationName}",
"xpack.synthetics.rules.tls.description": "シンセティック監視のTLS証明書の有効期限が近いときにアラートを発行します。",
"xpack.synthetics.rules.tls.expiredLabel": "有効期限切れです",
"xpack.synthetics.rules.tls.expiringLabel": "まもなく有効期限切れです",
@@ -47439,4 +47421,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。",
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
}
-}
+}
\ No newline at end of file
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 1a9f83673da10..fdf02dcc6a752 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -35036,7 +35036,6 @@
"xpack.securitySolution.appLinks.network.flows": "流",
"xpack.securitySolution.appLinks.network.http": "HTTP",
"xpack.securitySolution.appLinks.network.tls": "TLS",
- "xpack.securitySolution.appLinks.notesManagementDescription": "可视化并删除备注。",
"xpack.securitySolution.appLinks.overview": "概览",
"xpack.securitySolution.appLinks.overviewDescription": "您的安全环境活动摘要,包括告警、事件、最近项和新闻源!",
"xpack.securitySolution.appLinks.policiesDescription": "使用策略定制终端和云工作负载防护及其他配置。",
@@ -37558,7 +37557,6 @@
"xpack.securitySolution.endpoint.list.loadingPolicies": "正在加载集成",
"xpack.securitySolution.endpoint.list.noEndpointsInstructions": "您已添加 Elastic Defend 集成。现在,按照以下步骤注册您的代理。",
"xpack.securitySolution.endpoint.list.noEndpointsPrompt": "下一步:将代理注册到 Elastic Defend",
- "xpack.securitySolution.endpoint.list.noPolicies": "没有集成。",
"xpack.securitySolution.endpoint.list.os": "OS",
"xpack.securitySolution.endpoint.list.pageSubTitle": "运行 Elastic Defend 的主机",
"xpack.securitySolution.endpoint.list.pageTitle": "终端",
@@ -39293,7 +39291,6 @@
"xpack.securitySolution.navigation.manage": "管理",
"xpack.securitySolution.navigation.network": "网络",
"xpack.securitySolution.navigation.newRuleTitle": "创建新规则",
- "xpack.securitySolution.navigation.notesManagement": "备注",
"xpack.securitySolution.navigation.overview": "概览",
"xpack.securitySolution.navigation.protectionUpdates": "防护更新",
"xpack.securitySolution.navigation.responseActionsHistory": "响应操作历史记录",
@@ -42498,8 +42495,6 @@
"xpack.spaces.management.deselectAllFeaturesLink": "全部隐藏",
"xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "类别切换",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "设置功能可见性",
- "xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText": "管理安全角色",
- "xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "将会从用户界面移除隐藏的功能,但不会禁用。要获取功能的访问权限,{manageRolesLink}。",
"xpack.spaces.management.featureAccordionSwitchLabel": "{enabledCount}/{featureCount} 个功能可见",
"xpack.spaces.management.featureVisibilityTitle": "功能可见性",
"xpack.spaces.management.hideAllFeaturesText": "全部隐藏",
@@ -42511,15 +42506,11 @@
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "创建工作区",
"xpack.spaces.management.manageSpacePage.describeSpaceDescription": "为您的工作区提供好记的名称。",
"xpack.spaces.management.manageSpacePage.describeSpaceTitle": "描述此工作区",
- "xpack.spaces.management.manageSpacePage.editSpaceTitle": "编辑工作区",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "加载空间时出错:{message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "保存空间时出错:{message}",
- "xpack.spaces.management.manageSpacePage.featuresTitle": "功能",
- "xpack.spaces.management.manageSpacePage.generalTitle": "常规",
"xpack.spaces.management.manageSpacePage.loadErrorTitle": "加载可用功能时出错",
"xpack.spaces.management.manageSpacePage.loadingMessage": "正在加载……",
"xpack.spaces.management.manageSpacePage.nameFormRowLabel": "名称",
- "xpack.spaces.management.manageSpacePage.navigationTitle": "导航",
"xpack.spaces.management.manageSpacePage.optionalLabel": "可选",
"xpack.spaces.management.manageSpacePage.setSolutionViewDescription": "确定所有用户将在此工作区看到的导航。每个解决方案视图均包含来自分析工具的功能和管理功能。",
"xpack.spaces.management.manageSpacePage.setSolutionViewMessage": "设置解决方案视图",
@@ -42530,7 +42521,6 @@
"xpack.spaces.management.manageSpacePage.spaceDescriptionFormRowLabel": "描述",
"xpack.spaces.management.manageSpacePage.spaceDescriptionHelpText": "描述显示在“工作区选择”屏幕上。",
"xpack.spaces.management.manageSpacePage.spaceSuccessfullySavedNotificationMessage": "空间 “{name}” 已保存。",
- "xpack.spaces.management.manageSpacePage.updateSpaceButton": "更新工作区",
"xpack.spaces.management.navigation.solutionViewLabel": "解决方案视图",
"xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "保留的工作区是内置的,只能进行部分修改。",
"xpack.spaces.management.selectAllFeaturesLink": "全部显示",
@@ -43673,11 +43663,9 @@
"xpack.synthetics.alertRules.monitorStatus.actionVariables.state.status": "监测状态(例如“关闭”)。",
"xpack.synthetics.alertRules.monitorStatus.browser.label": "浏览器",
"xpack.synthetics.alertRules.monitorStatus.host.label": "主机",
- "xpack.synthetics.alertRules.monitorStatus.reasonMessage": "来自 {location} 的监测“{name}”为 {status} 状态。已于 {checkedAt} 检查。",
"xpack.synthetics.alertRules.monitorStatus.unavailableUrlLabel": "(不可用)",
"xpack.synthetics.alerts.monitorStatus.absoluteLink.label": "- 链接",
"xpack.synthetics.alerts.monitorStatus.defaultRecovery.status": "已恢复",
- "xpack.synthetics.alerts.monitorStatus.deleteMonitor.reason": "此监测已删除",
"xpack.synthetics.alerts.monitorStatus.deleteMonitor.status": "已删除",
"xpack.synthetics.alerts.monitorStatus.downLabel": "关闭",
"xpack.synthetics.alerts.monitorStatus.relativeLink.label": "- 相对链接",
@@ -43687,10 +43675,6 @@
"xpack.synthetics.alerts.monitorStatus.upCheck.status": "现已打开",
"xpack.synthetics.alerts.settings.addConnector": "添加连接器",
"xpack.synthetics.alerts.syntheticsMonitorStatus.clientName": "监测状态",
- "xpack.synthetics.alerts.syntheticsMonitorStatus.defaultActionMessage": "来自 {locationName} 的“{monitorName}”为 {status}。- Elastic Synthetics\n\n详情:\n\n- 监测名称:{monitorName} \n- {monitorUrlLabel}:{monitorUrl} \n- 监测类型:{monitorType} \n- 检查时间:{checkedAt} \n- 来自:{locationName} \n- 收到错误:{lastErrorMessage} \n{linkMessage}",
- "xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoveryMessage": "来自 {locationName} 的“{monitorName}”的告警不再处于活动状态:{recoveryReason}。- Elastic Synthetics\n\n详情:\n\n- 监测名称:{monitorName} \n- {monitorUrlLabel}:{monitorUrl} \n- 监测类型:{monitorType} \n- 来自:{locationName} \n- 收到的上一个错误:{lastErrorMessage} \n{linkMessage}",
- "xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoverySubjectMessage": "“{monitorName}”({locationName}) {recoveryStatus} - Elastic Synthetics",
- "xpack.synthetics.alerts.syntheticsMonitorStatus.defaultSubjectMessage": "“{monitorName}”({locationName}) 已关闭 - Elastic Synthetics",
"xpack.synthetics.alerts.syntheticsMonitorStatus.description": "监测关闭时告警。",
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultRecoverySubjectMessage": "告警已解析证书 {commonName} - Elastic Synthetics",
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultSubjectMessage": "已针对证书 {commonName} 触发告警 - Elastic Synthetics",
@@ -44532,8 +44516,6 @@
"xpack.synthetics.rules.tls.agingLabel": "过旧",
"xpack.synthetics.rules.tls.clientName": "Synthetics TLS",
"xpack.synthetics.rules.tls.criteriaExpression.ariaLabel": "显示正由此告警监视的监测条件的表达式",
- "xpack.synthetics.rules.tls.defaultActionMessage": "TLS 证书 {commonName} {status} - Elastic Synthetics\n\n详情:\n\n- 摘要:{summary}\n- 常见名称:{commonName}\n- 颁发者:{issuer}\n- 监测:{monitorName} \n- 监测 URL:{monitorUrl} \n- 监测类型:{monitorType} \n- 来自:{locationName}",
- "xpack.synthetics.rules.tls.defaultRecoveryMessage": "监测“{monitorName}”的 TLS 告警已恢复 - Elastic Synthetics\n\n详情:\n\n- 摘要:{summary}\n- 新状态:{newStatus}\n- 之前的状态:{previousStatus}\n- 监测:{monitorName} \n- URL:{monitorUrl} \n- 监测类型:{monitorType} \n- 来自:{locationName}",
"xpack.synthetics.rules.tls.description": "Synthetics 监测的 TLS 证书即将到期时告警。",
"xpack.synthetics.rules.tls.expiredLabel": "已过期",
"xpack.synthetics.rules.tls.expiringLabel": "即将到期",
@@ -47493,4 +47475,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
-}
+}
\ No newline at end of file
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx
index a0742ebfd2bbd..a938aa6b7e6e5 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx
@@ -308,7 +308,7 @@ describe('actions_connectors_list', () => {
.at(4)
.find('div[data-test-subj="compatibility-content"]')
.text()
- ).toBe('Alerting RulesCases');
+ ).toBe('Alerting RulesCasesSecurity Solution');
expect(
wrapper
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx
index 6814fa11ddcae..808c9f8b1bae4 100644
--- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx
@@ -24,6 +24,7 @@ import { ClosablePopoverTitle } from './components';
import { IErrorObject } from '../../types';
export interface ForLastExpressionProps {
+ description?: string;
timeWindowSize?: number;
timeWindowUnit?: string;
errors: IErrorObject;
@@ -45,6 +46,12 @@ export interface ForLastExpressionProps {
display?: 'fullWidth' | 'inline';
}
+const FOR_LAST_LABEL = i18n.translate(
+ 'xpack.triggersActionsUI.common.expressionItems.forTheLast.descriptionLabel',
+ {
+ defaultMessage: 'for the last',
+ }
+);
export const ForLastExpression = ({
timeWindowSize,
timeWindowUnit = 's',
@@ -53,6 +60,7 @@ export const ForLastExpression = ({
onChangeWindowSize,
onChangeWindowUnit,
popupPosition,
+ description = FOR_LAST_LABEL,
}: ForLastExpressionProps) => {
const [alertDurationPopoverOpen, setAlertDurationPopoverOpen] = useState(false);
@@ -60,12 +68,7 @@ export const ForLastExpression = ({
0}
+ isInvalid={Number(errors.timeWindowSize?.length) > 0}
error={errors.timeWindowSize as string[]}
>
0}
+ isInvalid={Number(errors.timeWindowSize?.length) > 0}
min={0}
value={timeWindowSize || ''}
onChange={(e) => {
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx
index 63c516dfea57e..5080b902c7775 100644
--- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useState } from 'react';
+import React, { ReactNode, useState } from 'react';
import {
EuiExpression,
EuiPopover,
@@ -20,6 +20,7 @@ import { IErrorObject } from '../../types';
export interface ValueExpressionProps {
description: string;
value: number;
+ valueLabel?: string | ReactNode;
onChangeSelectedValue: (updatedValue: number) => void;
popupPosition?:
| 'upCenter'
@@ -41,6 +42,7 @@ export interface ValueExpressionProps {
export const ValueExpression = ({
description,
value,
+ valueLabel,
onChangeSelectedValue,
display = 'inline',
popupPosition,
@@ -53,7 +55,7 @@ export const ValueExpression = ({
{
diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts
index 1faadc6041634..943135565428f 100644
--- a/x-pack/test/alerting_api_integration/common/config.ts
+++ b/x-pack/test/alerting_api_integration/common/config.ts
@@ -354,6 +354,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
'--xpack.task_manager.allow_reading_invalid_state=false',
'--xpack.actions.queued.max=500',
`--xpack.stack_connectors.enableExperimental=${JSON.stringify(experimentalFeatures)}`,
+ '--xpack.uptime.service.password=test',
+ '--xpack.uptime.service.username=localKibanaIntegrationTestsUser',
+ '--xpack.uptime.service.devUrl=mockDevUrl',
+ '--xpack.uptime.service.manifestUrl=mockDevUrl',
],
},
};
diff --git a/x-pack/test/alerting_api_integration/observability/helpers/alerting_api_helper.ts b/x-pack/test/alerting_api_integration/observability/helpers/alerting_api_helper.ts
index 1a66aa2fcd9ed..c4ec1a9180541 100644
--- a/x-pack/test/alerting_api_integration/observability/helpers/alerting_api_helper.ts
+++ b/x-pack/test/alerting_api_integration/observability/helpers/alerting_api_helper.ts
@@ -7,6 +7,7 @@
import type { Client } from '@elastic/elasticsearch';
import type { Agent as SuperTestAgent } from 'supertest';
+import expect from '@kbn/expect';
import { ToolingLog } from '@kbn/tooling-log';
import { ThresholdParams } from '@kbn/observability-plugin/common/custom_threshold_rule/types';
import { refreshSavedObjectIndices } from './refresh_index';
@@ -62,7 +63,7 @@ export async function createRule({
logger: ToolingLog;
esClient: Client;
}) {
- const { body } = await supertest
+ const { body, status } = await supertest
.post(`/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send({
@@ -75,8 +76,9 @@ export async function createRule({
name,
rule_type_id: ruleTypeId,
actions,
- })
- .expect(200);
+ });
+
+ expect(status).to.eql(200, JSON.stringify(body));
await refreshSavedObjectIndices(esClient);
logger.debug(`Created rule id: ${body.id}`);
diff --git a/x-pack/test/alerting_api_integration/observability/helpers/alerting_wait_for_helpers.ts b/x-pack/test/alerting_api_integration/observability/helpers/alerting_wait_for_helpers.ts
index 17f45b8129d91..edc5ca8d35a60 100644
--- a/x-pack/test/alerting_api_integration/observability/helpers/alerting_wait_for_helpers.ts
+++ b/x-pack/test/alerting_api_integration/observability/helpers/alerting_wait_for_helpers.ts
@@ -14,6 +14,7 @@ import type {
SearchResponse,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { RetryService } from '@kbn/ftr-common-functional-services';
+import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { retry } from '../../common/retry';
const TIMEOUT = 70_000;
@@ -63,6 +64,7 @@ export async function waitForDocumentInIndex({
timeout = TIMEOUT,
retries = RETRIES,
retryDelay = RETRY_DELAY,
+ filters,
}: {
esClient: Client;
indexName: string;
@@ -72,6 +74,7 @@ export async function waitForDocumentInIndex({
timeout?: number;
retries?: number;
retryDelay?: number;
+ filters?: QueryDslQueryContainer[];
}): Promise>> {
return await retry>>({
testFn: async () => {
@@ -79,6 +82,15 @@ export async function waitForDocumentInIndex({
index: indexName,
rest_total_hits_as_int: true,
ignore_unavailable: true,
+ body: filters
+ ? {
+ query: {
+ bool: {
+ filter: filters,
+ },
+ },
+ }
+ : undefined,
});
if (!response.hits.total || (response.hits.total as number) < docCountTarget) {
logger.debug(`Document count is ${response.hits.total}, should be ${docCountTarget}`);
@@ -104,12 +116,16 @@ export async function waitForAlertInIndex({
ruleId,
retryService,
logger,
+ filters = [],
+ retryDelay,
}: {
esClient: Client;
indexName: string;
ruleId: string;
retryService: RetryService;
logger: ToolingLog;
+ filters?: QueryDslQueryContainer[];
+ retryDelay?: number;
}): Promise>> {
return await retry>>({
testFn: async () => {
@@ -117,14 +133,21 @@ export async function waitForAlertInIndex({
index: indexName,
body: {
query: {
- term: {
- 'kibana.alert.rule.uuid': ruleId,
+ bool: {
+ filter: [
+ {
+ term: {
+ 'kibana.alert.rule.uuid': ruleId,
+ },
+ },
+ ...filters,
+ ],
},
},
},
});
if (response.hits.hits.length === 0) {
- throw new Error('No hits found');
+ throw new Error(`No hits found for the ruleId: ${ruleId}`);
}
return response;
},
@@ -133,6 +156,6 @@ export async function waitForAlertInIndex({
retryService,
timeout: TIMEOUT,
retries: RETRIES,
- retryDelay: RETRY_DELAY,
+ retryDelay: retryDelay ?? RETRY_DELAY,
});
}
diff --git a/x-pack/test/alerting_api_integration/observability/index.ts b/x-pack/test/alerting_api_integration/observability/index.ts
index 812123dd96b13..547c05a46bfcd 100644
--- a/x-pack/test/alerting_api_integration/observability/index.ts
+++ b/x-pack/test/alerting_api_integration/observability/index.ts
@@ -21,7 +21,8 @@ export default function ({ loadTestFile }: any) {
loadTestFile(require.resolve('./custom_threshold_rule_data_view'));
});
describe('Synthetics', () => {
- loadTestFile(require.resolve('./synthetics_rule'));
+ loadTestFile(require.resolve('./synthetics/synthetics_default_rule'));
+ loadTestFile(require.resolve('./synthetics/custom_status_rule'));
});
});
}
diff --git a/x-pack/test/alerting_api_integration/observability/synthetics/custom_status_rule.ts b/x-pack/test/alerting_api_integration/observability/synthetics/custom_status_rule.ts
new file mode 100644
index 0000000000000..6600054e03ab9
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/observability/synthetics/custom_status_rule.ts
@@ -0,0 +1,1000 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import expect from '@kbn/expect';
+import moment from 'moment';
+import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
+import { StatusRuleParams } from '@kbn/synthetics-plugin/common/rules/status_rule';
+import { FtrProviderContext } from '../../common/ftr_provider_context';
+import { SyntheticsRuleHelper, SYNTHETICS_ALERT_ACTION_INDEX } from './synthetics_rule_helper';
+import { waitForDocumentInIndex } from '../helpers/alerting_wait_for_helpers';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getService }: FtrProviderContext) {
+ const server = getService('kibanaServer');
+ const retryService = getService('retry');
+ const ruleHelper = new SyntheticsRuleHelper(getService);
+ const logger = getService('log');
+ const esClient = getService('es');
+ const esDeleteAllIndices = getService('esDeleteAllIndices');
+ const supertest = getService('supertest');
+
+ describe('SyntheticsCustomStatusRule', () => {
+ const SYNTHETICS_RULE_ALERT_INDEX = '.alerts-observability.uptime.alerts-default';
+
+ before(async () => {
+ await server.savedObjects.cleanStandardList();
+ await esDeleteAllIndices([SYNTHETICS_ALERT_ACTION_INDEX]);
+ await ruleHelper.createIndexAction();
+ await supertest
+ .put(SYNTHETICS_API_URLS.SYNTHETICS_ENABLEMENT)
+ .set('kbn-xsrf', 'true')
+ .expect(200);
+ });
+
+ after(async () => {
+ await server.savedObjects.cleanStandardList();
+ await esDeleteAllIndices([SYNTHETICS_ALERT_ACTION_INDEX]);
+ await esClient.deleteByQuery({
+ index: SYNTHETICS_RULE_ALERT_INDEX,
+ query: { match_all: {} },
+ });
+ });
+
+ /* 1. create a monitor
+ 2. create a custom rule
+ 3. create a down check scenario
+ 4. verify alert
+ 5. create an up check scenario
+ 6. verify recovered alert
+ when verifying recovered alert check:
+ - reason
+ - recoveryReason
+ - recoveryStatus
+ - locationNames
+ - link message
+ - locationId
+ when down recovered alert check
+ - reason
+ - locationNames
+ - link message
+ - locationId
+ */
+
+ describe('NumberOfChecks - location threshold = 1 - grouped by location - 1 location down', () => {
+ let ruleId = '';
+ let monitor: any;
+ let docs: any[] = [];
+
+ it('creates a monitor', async () => {
+ monitor = await ruleHelper.addMonitor('Monitor check based at ' + moment().format('LLL'));
+ expect(monitor).to.have.property('id');
+
+ docs = await ruleHelper.makeSummaries({
+ monitor,
+ downChecks: 5,
+ });
+ });
+
+ it('creates a custom rule', async () => {
+ const params = {
+ condition: {
+ locationsThreshold: 1,
+ window: {
+ numberOfChecks: 5,
+ },
+ groupBy: 'locationId',
+ downThreshold: 5,
+ },
+ monitorIds: [monitor.id],
+ };
+ const rule = await ruleHelper.createCustomStatusRule({
+ params,
+ name: 'When down 5 times from 1 location',
+ });
+ ruleId = rule.id;
+ expect(rule.params).to.eql(params);
+ });
+
+ it('should trigger down alert', async function () {
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ });
+
+ const alert: any = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'active');
+ expect(alert['kibana.alert.reason']).to.eql(
+ `Monitor "${monitor.name}" from Dev Service is down. Monitor is down 5 times within the last 5 checks. Alert when 5 out of the last 5 checks are down from at least 1 location.`
+ );
+ const downResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ });
+ expect(downResponse.hits.hits[0]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" from Dev Service is down. Monitor is down 5 times within the last 5 checks. Alert when 5 out of the last 5 checks are down from at least 1 location.`
+ );
+ expect(downResponse.hits.hits[0]._source).property('locationNames', 'Dev Service');
+ expect(downResponse.hits.hits[0]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(downResponse.hits.hits[0]._source).property('locationId', 'dev');
+ });
+
+ it('should trigger recovered alert', async function () {
+ docs = await ruleHelper.makeSummaries({
+ monitor,
+ upChecks: 1,
+ });
+
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [{ term: { 'kibana.alert.status': 'recovered' } }],
+ });
+
+ const alert = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'recovered');
+ const recoveredResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ docCountTarget: 2,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ });
+ expect(recoveredResponse.hits.hits[1]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" from Dev Service is recovered. Alert when 5 out of the last 5 checks are down from at least 1 location.`
+ );
+ expect(recoveredResponse.hits.hits[1]._source).property('locationNames', 'Dev Service');
+ expect(recoveredResponse.hits.hits[1]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(recoveredResponse.hits.hits[1]._source).property('locationId', 'dev');
+ expect(recoveredResponse.hits.hits[1]._source).property(
+ 'recoveryReason',
+ `the monitor is now up again. It ran successfully at ${moment(docs[0]['@timestamp'])
+ .tz('UTC')
+ .format('MMM D, YYYY @ HH:mm:ss.SSS')}`
+ );
+ expect(recoveredResponse.hits.hits[1]._source).property('recoveryStatus', 'is now up');
+ });
+ });
+
+ describe('NumberOfChecks - Location threshold = 1 - grouped by location - 2 down locations', () => {
+ let ruleId = '';
+ let monitor: any;
+
+ it('creates a monitor', async () => {
+ monitor = await ruleHelper.addMonitor('Monitor location based at ' + moment().format('LT'));
+ expect(monitor).to.have.property('id');
+ });
+
+ it('creates a custom rule with 1 location threshold grouped by location', async () => {
+ const params: StatusRuleParams = {
+ condition: {
+ window: {
+ numberOfChecks: 1,
+ },
+ groupBy: 'locationId',
+ locationsThreshold: 1,
+ downThreshold: 1,
+ },
+ monitorIds: [monitor.id],
+ };
+
+ const rule = await ruleHelper.createCustomStatusRule({
+ params,
+ });
+ ruleId = rule.id;
+ expect(rule.params).to.eql(params);
+
+ await ruleHelper.makeSummaries({
+ monitor,
+ downChecks: 1,
+ location: {
+ id: 'dev2',
+ label: 'Dev Service 2',
+ },
+ });
+ const downDocs = await ruleHelper.makeSummaries({
+ monitor,
+ downChecks: 1,
+ });
+
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [
+ { term: { 'kibana.alert.status': 'active' } },
+ { term: { 'monitor.id': monitor.id } },
+ { range: { '@timestamp': { gte: downDocs[0]['@timestamp'] } } },
+ ],
+ });
+
+ response.hits.hits.forEach((hit: any) => {
+ const alert: any = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'active');
+ expect(alert['kibana.alert.reason']).to.eql(
+ `Monitor "${monitor.name}" from ${alert['location.name']} is down. Monitor is down 1 time within the last 1 checks. Alert when 1 out of the last 1 checks are down from at least 1 location.`
+ );
+ });
+
+ const downResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ locationNames: string;
+ locationId: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ docCountTarget: 2,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ });
+ const lastTwoHits = downResponse.hits.hits.slice(-2).map((hit) => hit._source);
+
+ lastTwoHits.forEach((hit) => {
+ expect(hit).property(
+ 'reason',
+ `Monitor "${monitor.name}" from ${hit?.locationNames} is down. Monitor is down 1 time within the last 1 checks. Alert when 1 out of the last 1 checks are down from at least 1 location.`
+ );
+ expect(hit).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=${hit?.locationId}`
+ );
+ });
+ });
+ });
+
+ describe('NumberOfChecks - location threshold = 1 - ungrouped - 1 down location', () => {
+ let ruleId = '';
+ let monitor: any;
+ let docs: any[] = [];
+
+ it('creates a monitor', async () => {
+ monitor = await ruleHelper.addMonitor(
+ `Monitor check based at ${moment().format('LLL')} ungrouped`
+ );
+ expect(monitor).to.have.property('id');
+ });
+
+ it('creates a custom rule with 1 location threshold ungrouped', async () => {
+ const params = {
+ condition: {
+ locationsThreshold: 1,
+ window: {
+ numberOfChecks: 5,
+ },
+ groupBy: 'none',
+ downThreshold: 5,
+ },
+ monitorIds: [monitor.id],
+ };
+ const rule = await ruleHelper.createCustomStatusRule({
+ params,
+ name: 'Status based on number of checks',
+ });
+ ruleId = rule.id;
+ expect(rule.params).to.eql(params);
+ });
+
+ it('should trigger down for ungrouped', async () => {
+ docs = await ruleHelper.makeSummaries({
+ monitor,
+ downChecks: 5,
+ });
+
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [
+ { term: { 'kibana.alert.status': 'active' } },
+ { term: { 'monitor.id': monitor.id } },
+ { range: { '@timestamp': { gte: docs[0]['@timestamp'] } } },
+ ],
+ });
+
+ const alert: any = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'active');
+ expect(alert['kibana.alert.reason']).to.eql(
+ `Monitor "${monitor.name}" is down 5 times from Dev Service. Alert when down 5 times out of the last 5 checks from at least 1 location.`
+ );
+ const downResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ docCountTarget: 1,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ });
+ expect(downResponse.hits.hits[0]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" is down 5 times from Dev Service. Alert when down 5 times out of the last 5 checks from at least 1 location.`
+ );
+ expect(downResponse.hits.hits[0]._source).property('locationNames', 'Dev Service');
+ expect(downResponse.hits.hits[0]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(downResponse.hits.hits[0]._source).property('locationId', 'dev');
+ });
+
+ it('should trigger recovered alert', async () => {
+ const upDocs = await ruleHelper.makeSummaries({
+ monitor,
+ upChecks: 1,
+ });
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [
+ { term: { 'kibana.alert.status': 'recovered' } },
+ { term: { 'monitor.id': monitor.id } },
+ { range: { '@timestamp': { gte: upDocs[0]['@timestamp'] } } },
+ ],
+ });
+ expect(response.hits.hits?.[0]._source).property('kibana.alert.status', 'recovered');
+ const recoveredResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ docCountTarget: 2,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ });
+ expect(recoveredResponse.hits.hits[1]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" from Dev Service is recovered. Alert when 5 out of the last 5 checks are down from at least 1 location.`
+ );
+ expect(recoveredResponse.hits.hits[1]._source).property('locationNames', 'Dev Service');
+ expect(recoveredResponse.hits.hits[1]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(recoveredResponse.hits.hits[1]._source).property('locationId', 'dev');
+ expect(recoveredResponse.hits.hits[1]._source).property(
+ 'recoveryReason',
+ 'the alert condition is no longer met'
+ );
+ expect(recoveredResponse.hits.hits[1]._source).property('recoveryStatus', 'has recovered');
+ });
+ });
+
+ describe('NumberOfChecks - Location threshold > 1 - ungrouped - 2 down locations', () => {
+ let ruleId = '';
+ let monitor: any;
+
+ it('creates a monitor', async () => {
+ monitor = await ruleHelper.addMonitor(
+ `Monitor location based at ${moment().format('LT')} ungrouped 2 locations`
+ );
+ expect(monitor).to.have.property('id');
+ });
+
+ it('creates a custom rule with location threshold', async () => {
+ const params: StatusRuleParams = {
+ condition: {
+ locationsThreshold: 2,
+ window: {
+ numberOfChecks: 1,
+ },
+ groupBy: 'none',
+ downThreshold: 1,
+ },
+ monitorIds: [monitor.id],
+ };
+ const rule = await ruleHelper.createCustomStatusRule({
+ params,
+ name: 'When down from 2 locations',
+ });
+ ruleId = rule.id;
+ expect(rule.params).to.eql(params);
+ });
+
+ it('should not trigger down alert based on location threshold with one location down', async () => {
+ // first down check from dev 1
+ const docs = await ruleHelper.makeSummaries({
+ monitor,
+ downChecks: 1,
+ });
+ // ensure alert does not fire
+ try {
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [
+ { term: { 'kibana.alert.status': 'active' } },
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ {
+ range: {
+ '@timestamp': {
+ gte: docs[0]['@timestamp'],
+ },
+ },
+ },
+ ],
+ });
+ const alert: any = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'active');
+ expect(alert['kibana.alert.reason']).to.eql(
+ `Monitor "${monitor.name}" is down from 1 location (Dev Service). Alert when monitor is down from 1 location.`
+ );
+ throw new Error('Alert was triggered when condition should not be met');
+ } catch (e) {
+ if (e.message === 'Alert was triggered when condition should not be met') {
+ throw e;
+ }
+ }
+ });
+
+ it('should trigger down alert based on location threshold with two locations down', async () => {
+ // 1st down check from dev 2
+ await ruleHelper.makeSummaries({
+ monitor,
+ downChecks: 1,
+ location: {
+ id: 'dev2',
+ label: 'Dev Service 2',
+ },
+ });
+ // 2nd down check from dev 1
+ const downDocs = await ruleHelper.makeSummaries({
+ monitor,
+ downChecks: 1,
+ });
+
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [
+ { term: { 'kibana.alert.status': 'active' } },
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ {
+ range: {
+ '@timestamp': {
+ gte: downDocs[0]['@timestamp'],
+ },
+ },
+ },
+ ],
+ });
+ const alert: any = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'active');
+
+ expect(alert['kibana.alert.reason']).to.eql(
+ `Monitor "${monitor.name}" is down 1 time from Dev Service and 1 time from Dev Service 2. Alert when down 1 time out of the last 1 checks from at least 2 locations.`
+ );
+ const downResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ });
+ expect(downResponse.hits.hits[0]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" is down 1 time from Dev Service and 1 time from Dev Service 2. Alert when down 1 time out of the last 1 checks from at least 2 locations.`
+ );
+ expect(downResponse.hits.hits[0]._source).property(
+ 'locationNames',
+ 'Dev Service and Dev Service 2'
+ );
+ expect(downResponse.hits.hits[0]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(downResponse.hits.hits[0]._source).property('locationId', 'dev and dev2');
+ });
+
+ it('should trigger recovered alert', async () => {
+ const docs = await ruleHelper.makeSummaries({
+ monitor,
+ upChecks: 1,
+ });
+
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [
+ { term: { 'kibana.alert.status': 'recovered' } },
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ {
+ range: {
+ '@timestamp': {
+ gte: docs[0]['@timestamp'],
+ },
+ },
+ },
+ ],
+ });
+
+ const alert = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'recovered');
+ const recoveryResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ docCountTarget: 2,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ });
+ expect(recoveryResponse.hits.hits[1]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" from Dev Service and Dev Service 2 is recovered. Alert when 1 out of the last 1 checks are down from at least 2 locations.`
+ );
+ expect(recoveryResponse.hits.hits[1]._source).property(
+ 'locationNames',
+ 'Dev Service and Dev Service 2'
+ );
+ expect(recoveryResponse.hits.hits[1]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(recoveryResponse.hits.hits[1]._source).property('locationId', 'dev and dev2');
+ });
+
+ let downDocs: any[] = [];
+
+ it('should be down again', async () => {
+ downDocs = await ruleHelper.makeSummaries({
+ monitor,
+ downChecks: 1,
+ });
+
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [
+ { term: { 'kibana.alert.status': 'active' } },
+ { term: { 'monitor.id': monitor.id } },
+ { range: { '@timestamp': { gte: downDocs[0]['@timestamp'] } } },
+ ],
+ });
+
+ const alert: any = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'active');
+ expect(alert['kibana.alert.reason']).to.eql(
+ `Monitor "${monitor.name}" is down 1 time from Dev Service and 1 time from Dev Service 2. Alert when down 1 time out of the last 1 checks from at least 2 locations.`
+ );
+ const downResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ docCountTarget: 3,
+ filters: [{ term: { 'monitor.id': monitor.id } }],
+ });
+ expect(downResponse.hits.hits[2]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" is down 1 time from Dev Service and 1 time from Dev Service 2. Alert when down 1 time out of the last 1 checks from at least 2 locations.`
+ );
+ expect(downResponse.hits.hits[2]._source).property(
+ 'locationNames',
+ 'Dev Service and Dev Service 2'
+ );
+ expect(downResponse.hits.hits[2]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(downResponse.hits.hits[2]._source).property('locationId', 'dev and dev2');
+ });
+
+ it('should trigger recovered alert when the location threshold is no longer met', async () => {
+ // 2nd down check from dev 1
+ const upDocs = await ruleHelper.makeSummaries({
+ monitor,
+ upChecks: 1,
+ });
+
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [
+ { term: { 'kibana.alert.status': 'recovered' } },
+ { term: { 'monitor.id': monitor.id } },
+ { range: { '@timestamp': { gte: upDocs[0]['@timestamp'] } } },
+ ],
+ });
+ const alert = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'recovered');
+ const recoveryResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ docCountTarget: 4,
+ filters: [{ term: { 'monitor.id': monitor.id } }],
+ });
+ expect(recoveryResponse.hits.hits[3]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" from Dev Service and Dev Service 2 is recovered. Alert when 1 out of the last 1 checks are down from at least 2 locations.`
+ );
+ expect(recoveryResponse.hits.hits[3]._source).property(
+ 'locationNames',
+ 'Dev Service and Dev Service 2'
+ );
+ expect(recoveryResponse.hits.hits[3]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(recoveryResponse.hits.hits[3]._source).property('locationId', 'dev and dev2');
+ expect(recoveryResponse.hits.hits[3]._source).property(
+ 'recoveryReason',
+ 'the alert condition is no longer met'
+ );
+ expect(recoveryResponse.hits.hits[3]._source).property('recoveryStatus', 'has recovered');
+ });
+ });
+
+ describe('TimeWindow - Location threshold = 1 - grouped by location - 1 down location', () => {
+ let ruleId = '';
+ let monitor: any;
+
+ it('creates a monitor', async () => {
+ monitor = await ruleHelper.addMonitor('Monitor time based at ' + moment().format('LT'));
+ expect(monitor).to.have.property('id');
+ });
+
+ it('creates a custom rule with time based window', async () => {
+ const params: StatusRuleParams = {
+ condition: {
+ locationsThreshold: 1,
+ window: {
+ time: {
+ unit: 'm',
+ size: 5,
+ },
+ },
+ groupBy: 'locationId',
+ downThreshold: 5,
+ },
+ monitorIds: [monitor.id],
+ };
+ const rule = await ruleHelper.createCustomStatusRule({
+ params,
+ name: 'Status based on checks in a time window',
+ });
+ expect(rule).to.have.property('id');
+ ruleId = rule.id;
+ expect(rule.params).to.eql(params);
+ });
+
+ it('should trigger down alert', async function () {
+ await ruleHelper.makeSummaries({
+ monitor,
+ downChecks: 5,
+ });
+
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [{ term: { 'monitor.id': monitor.id } }],
+ });
+
+ const alert: any = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'active');
+ expect(alert['kibana.alert.reason']).to.eql(
+ `Monitor "${monitor.name}" from Dev Service is down. Alert when 5 checks are down within the last 5 minutes from at least 1 location.`
+ );
+ const downResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ });
+ expect(downResponse.hits.hits[0]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" from Dev Service is down. Alert when 5 checks are down within the last 5 minutes from at least 1 location.`
+ );
+ expect(downResponse.hits.hits[0]._source).property('locationNames', 'Dev Service');
+ expect(downResponse.hits.hits[0]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(downResponse.hits.hits[0]._source).property('locationId', 'dev');
+ });
+
+ it('should trigger recovered alert', async function () {
+ // wait 1 minute for at least 1 down check to fall out of the time window
+ await new Promise((resolve) => setTimeout(resolve, 30_000));
+
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [
+ { term: { 'kibana.alert.status': 'recovered' } },
+ { term: { 'monitor.id': monitor.id } },
+ ],
+ });
+
+ const alert = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'recovered');
+ const recoveryResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ docCountTarget: 2,
+ });
+ expect(recoveryResponse.hits.hits[1]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" from Dev Service is recovered. Alert when 5 checks are down within the last 5 minutes from at least 1 location.`
+ );
+ expect(recoveryResponse.hits.hits[1]._source).property(
+ 'recoveryReason',
+ 'the alert condition is no longer met'
+ );
+ expect(recoveryResponse.hits.hits[1]._source).property('recoveryStatus', 'has recovered');
+ expect(recoveryResponse.hits.hits[1]._source).property('locationNames', 'Dev Service');
+ expect(recoveryResponse.hits.hits[1]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(recoveryResponse.hits.hits[1]._source).property('locationId', 'dev');
+ });
+ });
+
+ describe('TimeWindow - Location threshold = 1 - grouped by location - 2 down location', () => {
+ let ruleId = '';
+ let monitor: any;
+
+ it('creates a monitor', async () => {
+ monitor = await ruleHelper.addMonitor(
+ `Monitor time based at ${moment().format('LT')} grouped 2 locations`
+ );
+ expect(monitor).to.have.property('id');
+ });
+
+ it('creates a custom rule with time based window', async () => {
+ const params: StatusRuleParams = {
+ condition: {
+ window: {
+ time: {
+ unit: 'm',
+ size: 5,
+ },
+ },
+ groupBy: 'locationId',
+ locationsThreshold: 2,
+ downThreshold: 5,
+ },
+ monitorIds: [monitor.id],
+ };
+ const rule = await ruleHelper.createCustomStatusRule({
+ params,
+ name: 'Status based on checks in a time window when down from 2 locations',
+ });
+ expect(rule).to.have.property('id');
+ ruleId = rule.id;
+ expect(rule.params).to.eql(params);
+ });
+
+ it('should trigger down alert', async function () {
+ // Generate data for 2 locations
+ await ruleHelper.makeSummaries({
+ monitor,
+ downChecks: 5,
+ location: monitor.locations[0],
+ });
+ await ruleHelper.makeSummaries({
+ monitor,
+ downChecks: 5,
+ location: monitor.locations[1],
+ });
+
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [{ term: { 'monitor.id': monitor.id } }],
+ });
+
+ const alert: any = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'active');
+ expect(alert['kibana.alert.reason']).to.eql(
+ `Monitor "${monitor.name}" is down 5 times from Dev Service and 5 times from Dev Service 2. Alert when down 5 times within the last 5 minutes from at least 2 locations.`
+ );
+ const downResponse = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ });
+
+ expect(downResponse.hits.hits[0]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" is down 5 times from Dev Service and 5 times from Dev Service 2. Alert when down 5 times within the last 5 minutes from at least 2 locations.`
+ );
+ expect(downResponse.hits.hits[0]._source).property(
+ 'locationNames',
+ 'Dev Service and Dev Service 2'
+ );
+ expect(downResponse.hits.hits[0]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(downResponse.hits.hits[0]._source).property('locationId', 'dev and dev2');
+ });
+
+ it('should trigger alert action', async function () {
+ const alertAction = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ docCountTarget: 1,
+ });
+
+ expect(alertAction.hits.hits[0]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" is down 5 times from Dev Service and 5 times from Dev Service 2. Alert when down 5 times within the last 5 minutes from at least 2 locations.`
+ );
+ expect(alertAction.hits.hits[0]._source).property(
+ 'locationNames',
+ 'Dev Service and Dev Service 2'
+ );
+ expect(alertAction.hits.hits[0]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(alertAction.hits.hits[0]._source).property('locationId', 'dev and dev2');
+ });
+
+ it('should trigger recovered alert', async function () {
+ // wait 30 secs for at least 1 down check to fall out of the time window
+ await new Promise((resolve) => setTimeout(resolve, 30_000));
+
+ const response = await ruleHelper.waitForStatusAlert({
+ ruleId,
+ filters: [
+ { term: { 'kibana.alert.status': 'recovered' } },
+ { term: { 'monitor.id': monitor.id } },
+ ],
+ });
+
+ const alert = response.hits.hits?.[0]._source;
+ expect(alert).to.have.property('kibana.alert.status', 'recovered');
+ const recoveryAction = await waitForDocumentInIndex<{
+ ruleType: string;
+ alertDetailsUrl: string;
+ reason: string;
+ }>({
+ esClient,
+ indexName: SYNTHETICS_ALERT_ACTION_INDEX,
+ retryService,
+ logger,
+ filters: [
+ {
+ term: { 'monitor.id': monitor.id },
+ },
+ ],
+ docCountTarget: 2,
+ });
+
+ expect(recoveryAction.hits.hits[1]._source).property(
+ 'reason',
+ `Monitor "${monitor.name}" from Dev Service and Dev Service 2 is recovered. Alert when 5 checks are down within the last 5 minutes from at least 2 locations.`
+ );
+ expect(recoveryAction.hits.hits[1]._source).property(
+ 'recoveryReason',
+ 'the alert condition is no longer met'
+ );
+ expect(recoveryAction.hits.hits[1]._source).property('recoveryStatus', 'has recovered');
+ expect(recoveryAction.hits.hits[1]._source).property(
+ 'locationNames',
+ 'Dev Service and Dev Service 2'
+ );
+ expect(recoveryAction.hits.hits[1]._source).property(
+ 'linkMessage',
+ `- Link: https://localhost:5601/app/synthetics/monitor/${monitor.id}/errors/Test%20private%20location-18524a3d9a7-0?locationId=dev`
+ );
+ expect(recoveryAction.hits.hits[1]._source).property('locationId', 'dev and dev2');
+ });
+ });
+
+ // TimeWindow - Location threshold = 1 - ungrouped - 1 down location
+
+ // TimeWindow - Location threshold > 1 - ungrouped - 2 down locations
+ });
+}
diff --git a/x-pack/test/alerting_api_integration/observability/synthetics_rule.ts b/x-pack/test/alerting_api_integration/observability/synthetics/data.ts
similarity index 59%
rename from x-pack/test/alerting_api_integration/observability/synthetics_rule.ts
rename to x-pack/test/alerting_api_integration/observability/synthetics/data.ts
index 864d73a991e50..566bb410d6321 100644
--- a/x-pack/test/alerting_api_integration/observability/synthetics_rule.ts
+++ b/x-pack/test/alerting_api_integration/observability/synthetics/data.ts
@@ -5,132 +5,13 @@
* 2.0.
*/
-import expect from '@kbn/expect';
-import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
-import { SanitizedRule } from '@kbn/alerting-plugin/common';
-import { omit } from 'lodash';
-import { TlsTranslations } from '@kbn/synthetics-plugin/common/rules/synthetics/translations';
-import { FtrProviderContext } from '../common/ftr_provider_context';
+import {
+ SyntheticsMonitorStatusTranslations,
+ TlsTranslations,
+} from '@kbn/synthetics-plugin/common/rules/synthetics/translations';
+import { SanitizedRule } from '@kbn/alerting-types';
-// eslint-disable-next-line import/no-default-export
-export default function ({ getService }: FtrProviderContext) {
- const supertest = getService('supertest');
- const server = getService('kibanaServer');
-
- const testActions = [
- 'custom.ssl.noCustom',
- 'notification-email',
- 'preconfigured-es-index-action',
- 'my-deprecated-servicenow',
- 'my-slack1',
- ];
-
- describe('SyntheticsRules', () => {
- before(async () => {
- await server.savedObjects.cleanStandardList();
- });
-
- after(async () => {
- await server.savedObjects.cleanStandardList();
- });
-
- it('creates rule when settings are configured', async () => {
- await supertest
- .put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
- .set('kbn-xsrf', 'true')
- .send({
- certExpirationThreshold: 30,
- certAgeThreshold: 730,
- defaultConnectors: testActions.slice(0, 2),
- defaultEmail: { to: ['test@gmail.com'], cc: [], bcc: [] },
- })
- .expect(200);
-
- const response = await supertest
- .post(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
- .set('kbn-xsrf', 'true')
- .send();
- const statusResult = response.body.statusRule;
- const tlsResult = response.body.tlsRule;
- expect(statusResult.actions.length).eql(4);
- expect(tlsResult.actions.length).eql(4);
-
- compareRules(statusResult, statusRule);
- compareRules(tlsResult, tlsRule);
-
- testActions.slice(0, 2).forEach((action) => {
- const { recoveredAction, firingAction } = getActionById(statusRule, action);
- const resultAction = getActionById(statusResult, action);
- expect(firingAction).eql(resultAction.firingAction);
- expect(recoveredAction).eql(resultAction.recoveredAction);
- });
-
- testActions.slice(0, 2).forEach((action) => {
- const { recoveredAction, firingAction } = getActionById(tlsRule, action);
- const resultAction = getActionById(tlsResult, action);
- expect(firingAction).eql(resultAction.firingAction);
- expect(recoveredAction).eql(resultAction.recoveredAction);
- });
- });
-
- it('updates rules when settings are updated', async () => {
- await supertest
- .put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
- .set('kbn-xsrf', 'true')
- .send({
- certExpirationThreshold: 30,
- certAgeThreshold: 730,
- defaultConnectors: testActions,
- defaultEmail: { to: ['test@gmail.com'], cc: [], bcc: [] },
- })
- .expect(200);
-
- const response = await supertest
- .put(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
- .set('kbn-xsrf', 'true')
- .send();
-
- const statusResult = response.body.statusRule;
- const tlsResult = response.body.tlsRule;
- expect(statusResult.actions.length).eql(9);
- expect(tlsResult.actions.length).eql(9);
-
- compareRules(statusResult, statusRule);
- compareRules(tlsResult, tlsRule);
-
- testActions.forEach((action) => {
- const { recoveredAction, firingAction } = getActionById(statusRule, action);
- const resultAction = getActionById(statusResult, action);
- expect(firingAction).eql(resultAction.firingAction);
- expect(recoveredAction).eql(resultAction.recoveredAction);
- });
- testActions.forEach((action) => {
- const { recoveredAction, firingAction } = getActionById(tlsRule, action);
- const resultAction = getActionById(tlsResult, action);
- expect(firingAction).eql(resultAction.firingAction);
- expect(recoveredAction).eql(resultAction.recoveredAction);
- });
- });
- });
-}
-const compareRules = (rule1: SanitizedRule, rule2: SanitizedRule) => {
- expect(rule1.alertTypeId).eql(rule2.alertTypeId);
- expect(rule1.schedule).eql(rule2.schedule);
-};
-
-const getActionById = (rule: SanitizedRule, id: string) => {
- const actions = rule.actions.filter((action) => action.id === id);
- const recoveredAction = actions.find(
- (action) => 'group' in action && action.group === 'recovered'
- );
- const firingAction = actions.find((action) => 'group' in action && action.group !== 'recovered');
- return {
- recoveredAction: omit(recoveredAction, ['uuid']),
- firingAction: omit(firingAction, ['uuid']),
- };
-};
-
-const statusRule = {
+export const statusRule = {
id: 'dbbc39f0-1781-11ee-80b9-6522650f1d50',
notifyWhen: null,
consumer: 'uptime',
@@ -152,7 +33,7 @@ const statusRule = {
{
group: 'recovered',
params: {
- body: 'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
+ body: SyntheticsMonitorStatusTranslations.defaultRecoveryMessage,
},
frequency: { notifyWhen: 'onActionGroupChange', throttle: null, summary: false },
uuid: '789f2b81-e098-4f33-9802-1d355f4fabbe',
@@ -162,7 +43,7 @@ const statusRule = {
{
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
params: {
- body: '"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
+ body: SyntheticsMonitorStatusTranslations.defaultActionMessage,
},
frequency: { notifyWhen: 'onActionGroupChange', throttle: null, summary: false },
uuid: '1b3f3958-f019-4ca0-b6b1-ccc4cf51d501',
@@ -173,10 +54,8 @@ const statusRule = {
group: 'recovered',
params: {
to: ['test@gmail.com'],
- subject:
- '"{{context.monitorName}}" ({{context.locationName}}) {{context.recoveryStatus}} - Elastic Synthetics',
- message:
- 'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
+ subject: SyntheticsMonitorStatusTranslations.defaultRecoverySubjectMessage,
+ message: SyntheticsMonitorStatusTranslations.defaultRecoveryMessage,
messageHTML: null,
cc: [],
bcc: [],
@@ -191,10 +70,8 @@ const statusRule = {
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
params: {
to: ['test@gmail.com'],
- subject:
- '"{{context.monitorName}}" ({{context.locationName}}) is down - Elastic Synthetics',
- message:
- '"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
+ subject: SyntheticsMonitorStatusTranslations.defaultSubjectMessage,
+ message: SyntheticsMonitorStatusTranslations.defaultActionMessage,
messageHTML: null,
cc: [],
bcc: [],
@@ -250,10 +127,8 @@ const statusRule = {
subAction: 'pushToService',
subActionParams: {
incident: {
- short_description:
- '"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
- description:
- '"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
+ short_description: SyntheticsMonitorStatusTranslations.defaultActionMessage,
+ description: SyntheticsMonitorStatusTranslations.defaultActionMessage,
impact: '2',
severity: '2',
urgency: '2',
@@ -275,8 +150,7 @@ const statusRule = {
{
group: 'recovered',
params: {
- message:
- 'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
+ message: SyntheticsMonitorStatusTranslations.defaultRecoveryMessage,
},
frequency: { notifyWhen: 'onActionGroupChange', throttle: null, summary: false },
uuid: '2d73f370-a90c-4347-8480-753cbeae719f',
@@ -286,8 +160,7 @@ const statusRule = {
{
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
params: {
- message:
- '"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
+ message: SyntheticsMonitorStatusTranslations.defaultActionMessage,
},
frequency: { notifyWhen: 'onActionGroupChange', throttle: null, summary: false },
uuid: '1c5d0dd1-c360-4e14-8e4f-f24aa5c640c6',
@@ -339,7 +212,8 @@ const statusRule = {
},
ruleTypeId: 'xpack.synthetics.alerts.monitorStatus',
} as unknown as SanitizedRule;
-const tlsRule = {
+
+export const tlsRule = {
id: 'dbbc12e0-1781-11ee-80b9-6522650f1d50',
notifyWhen: null,
consumer: 'uptime',
diff --git a/x-pack/test/alerting_api_integration/observability/synthetics/private_location_test_service.ts b/x-pack/test/alerting_api_integration/observability/synthetics/private_location_test_service.ts
new file mode 100644
index 0000000000000..e9ac7237dca52
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/observability/synthetics/private_location_test_service.ts
@@ -0,0 +1,83 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { v4 as uuidv4 } from 'uuid';
+import { privateLocationsSavedObjectName } from '@kbn/synthetics-plugin/common/saved_objects/private_locations';
+import { privateLocationsSavedObjectId } from '@kbn/synthetics-plugin/server/saved_objects/private_locations';
+import { SyntheticsPrivateLocations } from '@kbn/synthetics-plugin/common/runtime_types';
+import { Agent as SuperTestAgent } from 'supertest';
+import { FtrProviderContext } from '../../common/ftr_provider_context';
+
+export const INSTALLED_VERSION = '1.1.1';
+
+export class PrivateLocationTestService {
+ private supertest: SuperTestAgent;
+ private readonly getService: FtrProviderContext['getService'];
+
+ constructor(getService: FtrProviderContext['getService']) {
+ this.supertest = getService('supertest');
+ this.getService = getService;
+ }
+
+ async installSyntheticsPackage() {
+ await this.supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200);
+ const response = await this.supertest
+ .get(`/api/fleet/epm/packages/synthetics/${INSTALLED_VERSION}`)
+ .set('kbn-xsrf', 'true')
+ .expect(200);
+ if (response.body.item.status !== 'installed') {
+ await this.supertest
+ .post(`/api/fleet/epm/packages/synthetics/${INSTALLED_VERSION}`)
+ .set('kbn-xsrf', 'true')
+ .send({ force: true })
+ .expect(200);
+ }
+ }
+
+ async addTestPrivateLocation() {
+ const apiResponse = await this.addFleetPolicy(uuidv4());
+ const testPolicyId = apiResponse.body.item.id;
+ return (await this.setTestLocations([testPolicyId]))[0];
+ }
+
+ async addFleetPolicy(name: string) {
+ return this.supertest
+ .post('/api/fleet/agent_policies?sys_monitoring=true')
+ .set('kbn-xsrf', 'true')
+ .send({
+ name,
+ description: '',
+ namespace: 'default',
+ monitoring_enabled: [],
+ })
+ .expect(200);
+ }
+
+ async setTestLocations(testFleetPolicyIds: string[]) {
+ const server = this.getService('kibanaServer');
+
+ const locations: SyntheticsPrivateLocations = testFleetPolicyIds.map((id, index) => ({
+ label: 'Test private location ' + index,
+ agentPolicyId: id,
+ id,
+ geo: {
+ lat: 0,
+ lon: 0,
+ },
+ isServiceManaged: false,
+ }));
+
+ await server.savedObjects.create({
+ type: privateLocationsSavedObjectName,
+ id: privateLocationsSavedObjectId,
+ attributes: {
+ locations,
+ },
+ overwrite: true,
+ });
+ return locations;
+ }
+}
diff --git a/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_default_rule.ts b/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_default_rule.ts
new file mode 100644
index 0000000000000..39f36b71b52ee
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_default_rule.ts
@@ -0,0 +1,131 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
+import { SanitizedRule } from '@kbn/alerting-plugin/common';
+import { omit } from 'lodash';
+import { FtrProviderContext } from '../../common/ftr_provider_context';
+import { statusRule, tlsRule } from './data';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const server = getService('kibanaServer');
+
+ const testActions = [
+ 'custom.ssl.noCustom',
+ 'notification-email',
+ 'preconfigured-es-index-action',
+ 'my-deprecated-servicenow',
+ 'my-slack1',
+ ];
+
+ describe('SyntheticsDefaultRules', () => {
+ before(async () => {
+ await server.savedObjects.cleanStandardList();
+ });
+
+ after(async () => {
+ await server.savedObjects.cleanStandardList();
+ });
+
+ it('creates rule when settings are configured', async () => {
+ await supertest
+ .put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
+ .set('kbn-xsrf', 'true')
+ .send({
+ certExpirationThreshold: 30,
+ certAgeThreshold: 730,
+ defaultConnectors: testActions.slice(0, 2),
+ defaultEmail: { to: ['test@gmail.com'], cc: [], bcc: [] },
+ })
+ .expect(200);
+
+ const response = await supertest
+ .post(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
+ .set('kbn-xsrf', 'true')
+ .send();
+ const statusResult = response.body.statusRule;
+ const tlsResult = response.body.tlsRule;
+ expect(statusResult.actions.length).eql(4);
+ expect(tlsResult.actions.length).eql(4);
+
+ compareRules(statusResult, statusRule);
+ compareRules(tlsResult, tlsRule);
+
+ testActions.slice(0, 2).forEach((action) => {
+ const { recoveredAction, firingAction } = getActionById(statusRule, action);
+ const resultAction = getActionById(statusResult, action);
+ expect(firingAction).eql(resultAction.firingAction);
+ expect(recoveredAction).eql(resultAction.recoveredAction);
+ });
+
+ testActions.slice(0, 2).forEach((action) => {
+ const { recoveredAction, firingAction } = getActionById(tlsRule, action);
+ const resultAction = getActionById(tlsResult, action);
+ expect(firingAction).eql(resultAction.firingAction);
+ expect(recoveredAction).eql(resultAction.recoveredAction);
+ });
+ });
+
+ it('updates rules when settings are updated', async () => {
+ await supertest
+ .put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
+ .set('kbn-xsrf', 'true')
+ .send({
+ certExpirationThreshold: 30,
+ certAgeThreshold: 730,
+ defaultConnectors: testActions,
+ defaultEmail: { to: ['test@gmail.com'], cc: [], bcc: [] },
+ })
+ .expect(200);
+
+ const response = await supertest
+ .put(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
+ .set('kbn-xsrf', 'true')
+ .send();
+
+ const statusResult = response.body.statusRule;
+ const tlsResult = response.body.tlsRule;
+ expect(statusResult.actions.length).eql(9);
+ expect(tlsResult.actions.length).eql(9);
+
+ compareRules(statusResult, statusRule);
+ compareRules(tlsResult, tlsRule);
+
+ testActions.forEach((action) => {
+ const { recoveredAction, firingAction } = getActionById(statusRule, action);
+ const resultAction = getActionById(statusResult, action);
+ expect(firingAction).eql(resultAction.firingAction);
+ expect(recoveredAction).eql(resultAction.recoveredAction);
+ });
+ testActions.forEach((action) => {
+ const { recoveredAction, firingAction } = getActionById(tlsRule, action);
+ const resultAction = getActionById(tlsResult, action);
+ expect(firingAction).eql(resultAction.firingAction);
+ expect(recoveredAction).eql(resultAction.recoveredAction);
+ });
+ });
+ });
+}
+const compareRules = (rule1: SanitizedRule, rule2: SanitizedRule) => {
+ expect(rule1.alertTypeId).eql(rule2.alertTypeId);
+ expect(rule1.schedule).eql(rule2.schedule);
+};
+
+const getActionById = (rule: SanitizedRule, id: string) => {
+ const actions = rule.actions.filter((action) => action.id === id);
+ const recoveredAction = actions.find(
+ (action) => 'group' in action && action.group === 'recovered'
+ );
+ const firingAction = actions.find((action) => 'group' in action && action.group !== 'recovered');
+ return {
+ recoveredAction: omit(recoveredAction, ['uuid']),
+ firingAction: omit(firingAction, ['uuid']),
+ };
+};
diff --git a/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts b/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts
new file mode 100644
index 0000000000000..9321bc0935a80
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts
@@ -0,0 +1,292 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { StatusRuleParams } from '@kbn/synthetics-plugin/common/rules/status_rule';
+import type { Client } from '@elastic/elasticsearch';
+import { ToolingLog } from '@kbn/tooling-log';
+import { makeDownSummary, makeUpSummary } from '@kbn/observability-synthetics-test-data';
+import { RetryService } from '@kbn/ftr-common-functional-services';
+import { EncryptedSyntheticsSavedMonitor } from '@kbn/synthetics-plugin/common/runtime_types';
+import moment from 'moment';
+import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { Agent as SuperTestAgent } from 'supertest';
+import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
+import expect from '@kbn/expect';
+import { waitForAlertInIndex } from '../helpers/alerting_wait_for_helpers';
+import { FtrProviderContext } from '../../common/ftr_provider_context';
+import { PrivateLocationTestService } from './private_location_test_service';
+import { createIndexConnector, createRule } from '../helpers/alerting_api_helper';
+
+export const SYNTHETICS_ALERT_ACTION_INDEX = 'alert-action-synthetics';
+export class SyntheticsRuleHelper {
+ supertest: SuperTestAgent;
+ logger: ToolingLog;
+ esClient: Client;
+ retryService: RetryService;
+ locService: PrivateLocationTestService;
+ alertActionIndex: string;
+ actionId: string | null = null;
+
+ constructor(getService: FtrProviderContext['getService']) {
+ this.esClient = getService('es');
+ this.supertest = getService('supertest');
+ this.logger = getService('log');
+ this.retryService = getService('retry');
+ this.locService = new PrivateLocationTestService(getService);
+ this.alertActionIndex = SYNTHETICS_ALERT_ACTION_INDEX;
+ }
+
+ async createIndexAction() {
+ await this.esClient.indices.create({
+ index: this.alertActionIndex,
+ body: {
+ mappings: {
+ properties: {
+ 'monitor.id': {
+ type: 'keyword',
+ },
+ },
+ },
+ },
+ });
+ const actionId = await createIndexConnector({
+ supertest: this.supertest,
+ name: 'Index Connector: Synthetics API test',
+ indexName: this.alertActionIndex,
+ logger: this.logger,
+ });
+ this.actionId = actionId;
+ }
+
+ async createCustomStatusRule({
+ params,
+ name,
+ }: {
+ params: StatusRuleParams;
+ name?: string;
+ actions?: any[];
+ }) {
+ if (this.actionId === null) {
+ throw new Error('Index action not created. Call createIndexAction() first');
+ }
+ return await createRule({
+ params,
+ name: name ?? 'Custom status rule',
+ ruleTypeId: 'xpack.synthetics.alerts.monitorStatus',
+ consumer: 'alerts',
+ supertest: this.supertest,
+ esClient: this.esClient,
+ logger: this.logger,
+ schedule: { interval: '15s' },
+ actions: [
+ {
+ group: 'recovered',
+ id: this.actionId,
+ params: {
+ documents: [
+ {
+ status: 'recovered',
+ reason: '{{context.reason}}',
+ locationNames: '{{context.locationNames}}',
+ locationId: '{{context.locationId}}',
+ linkMessage: '{{context.linkMessage}}',
+ recoveryReason: '{{context.recoveryReason}}',
+ recoveryStatus: '{{context.recoveryStatus}}',
+ 'monitor.id': '{{context.monitorId}}',
+ },
+ ],
+ },
+ frequency: {
+ notify_when: 'onActionGroupChange',
+ throttle: null,
+ summary: false,
+ },
+ },
+ {
+ group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
+ id: this.actionId,
+ params: {
+ documents: [
+ {
+ status: 'active',
+ reason: '{{context.reason}}',
+ locationNames: '{{context.locationNames}}',
+ locationId: '{{context.locationId}}',
+ linkMessage: '{{context.linkMessage}}',
+ 'monitor.id': '{{context.monitorId}}',
+ },
+ ],
+ },
+ frequency: {
+ notify_when: 'onActionGroupChange',
+ throttle: null,
+ summary: false,
+ },
+ },
+ ],
+ });
+ }
+
+ async addMonitor(name: string) {
+ const testData = {
+ locations: [
+ { id: 'dev', isServiceManaged: true, label: 'Dev Service' },
+ { id: 'dev2', isServiceManaged: true, label: 'Dev Service 2' },
+ ],
+ name,
+ type: 'http',
+ url: 'http://www.google.com',
+ schedule: 1,
+ };
+ const res = await this.supertest
+ .post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS)
+ .set('kbn-xsrf', 'true')
+ .send(testData);
+
+ expect(res.status).to.eql(200, JSON.stringify(res.body));
+
+ return res.body as EncryptedSyntheticsSavedMonitor;
+ }
+
+ async deleteMonitor(monitorId: string) {
+ const res = await this.supertest
+ .delete(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
+ .set('kbn-xsrf', 'true')
+ .send();
+
+ expect(res.status).to.eql(200);
+ }
+
+ async updateTestMonitor(monitorId: string, updates: Record) {
+ const result = await this.supertest
+ .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `/${monitorId}`)
+ .set('kbn-xsrf', 'true')
+ .send(updates);
+
+ expect(result.status).to.eql(200, JSON.stringify(result.body));
+
+ return result.body as EncryptedSyntheticsSavedMonitor;
+ }
+
+ async addPrivateLocation() {
+ await this.locService.installSyntheticsPackage();
+ return this.locService.addTestPrivateLocation();
+ }
+
+ async waitForStatusAlert({
+ ruleId,
+ filters,
+ }: {
+ ruleId: string;
+ filters?: QueryDslQueryContainer[];
+ }) {
+ return await waitForAlertInIndex({
+ ruleId,
+ filters,
+ esClient: this.esClient,
+ retryService: this.retryService,
+ logger: this.logger,
+ indexName: '.internal.alerts-observability.uptime.alerts-default*',
+ retryDelay: 1000,
+ });
+ }
+
+ async makeSummaries({
+ downChecks = 0,
+ upChecks = 0,
+ monitor,
+ location,
+ }: {
+ downChecks?: number;
+ upChecks?: number;
+ monitor: EncryptedSyntheticsSavedMonitor;
+ location?: {
+ id: string;
+ label: string;
+ };
+ }) {
+ const docs = [];
+ // lets make some down checks
+ for (let i = downChecks; i > 0; i--) {
+ const doc = await this.addSummaryDocument({
+ monitor,
+ location,
+ status: 'down',
+ timestamp: moment()
+ .subtract(i - 1, 'minutes')
+ .toISOString(),
+ });
+ docs.push(doc);
+ }
+
+ // lets make some up checks
+ for (let i = upChecks; i > 0; i--) {
+ const doc = await this.addSummaryDocument({
+ monitor,
+ location,
+ status: 'up',
+ timestamp: moment()
+ .subtract(i - 1, 'minutes')
+ .toISOString(),
+ });
+ docs.push(doc);
+ }
+ return docs;
+ }
+
+ async addSummaryDocument({
+ monitor,
+ location,
+ status = 'up',
+ timestamp = new Date(Date.now()).toISOString(),
+ }: {
+ monitor: EncryptedSyntheticsSavedMonitor;
+ status?: 'up' | 'down';
+ timestamp?: string;
+ location?: {
+ id: string;
+ label: string;
+ };
+ }) {
+ let document = {
+ '@timestamp': timestamp,
+ };
+
+ const index = 'synthetics-http-default';
+
+ const commonData = {
+ timestamp,
+ location,
+ monitorId: monitor.id,
+ name: monitor.name,
+ configId: monitor.config_id,
+ };
+
+ if (status === 'down') {
+ document = {
+ ...makeDownSummary(commonData),
+ ...document,
+ };
+ } else {
+ document = {
+ ...makeUpSummary(commonData),
+ ...document,
+ };
+ }
+
+ this.logger.debug(
+ `created synthetics summary, status: ${status}, monitor: "${monitor.name}", location: "${location?.label}"`
+ );
+ await this.esClient.index({
+ index,
+ document,
+ refresh: true,
+ });
+
+ return document;
+ }
+}
diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts b/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts
index 9ab91cf0d60c0..51f98b5389a9d 100644
--- a/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts
+++ b/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts
@@ -29,6 +29,21 @@ export const deleteIndex = async (es: Client, indexToBeDeleted: string[]) => {
]);
};
+export const bulkIndex = async (es: Client, findingsMock: T[], indexName: string) => {
+ const operations = findingsMock.flatMap((finding) => [
+ { create: { _index: indexName } }, // Action description
+ {
+ ...finding,
+ '@timestamp': new Date().toISOString(),
+ }, // Data to index
+ ]);
+
+ await es.bulk({
+ body: operations, // Bulk API expects 'body' for operations
+ refresh: true,
+ });
+};
+
export const addIndex = async (es: Client, findingsMock: T[], indexName: string) => {
await Promise.all([
...findingsMock.map((finding) =>
diff --git a/x-pack/test/api_integration/apis/maps/maps_telemetry.ts b/x-pack/test/api_integration/apis/maps/maps_telemetry.ts
index acd846f77c982..3207fdbb739ef 100644
--- a/x-pack/test/api_integration/apis/maps/maps_telemetry.ts
+++ b/x-pack/test/api_integration/apis/maps/maps_telemetry.ts
@@ -36,8 +36,8 @@ export default function ({ getService }: FtrProviderContext) {
return fieldStat.name === 'geo_point';
}
);
- expect(geoPointFieldStats.count).to.be(47);
- expect(geoPointFieldStats.index_count).to.be(11);
+ expect(geoPointFieldStats.count).to.be(55);
+ expect(geoPointFieldStats.index_count).to.be(12);
const geoShapeFieldStats = apiResponse.cluster_stats.indices.mappings.field_types.find(
(fieldStat: estypes.ClusterStatsFieldTypes) => {
diff --git a/x-pack/test/api_integration/apis/slos/delete_slo.ts b/x-pack/test/api_integration/apis/slos/delete_slo.ts
index 19480d568a37c..979564f06be55 100644
--- a/x-pack/test/api_integration/apis/slos/delete_slo.ts
+++ b/x-pack/test/api_integration/apis/slos/delete_slo.ts
@@ -55,13 +55,15 @@ export default function ({ getService }: FtrProviderContext) {
const { id } = response.body;
- const savedObject = await kibanaServer.savedObjects.find({
- type: SO_SLO_TYPE,
- });
+ await retry.tryForTime(10000, async () => {
+ const savedObject = await kibanaServer.savedObjects.find({
+ type: SO_SLO_TYPE,
+ });
- expect(savedObject.saved_objects.length).eql(1);
+ expect(savedObject.saved_objects.length).eql(1);
- expect(savedObject.saved_objects[0].attributes.id).eql(id);
+ expect(savedObject.saved_objects[0].attributes.id).eql(id);
+ });
await retry.tryForTime(300 * 1000, async () => {
// expect summary and rollup data to exist
diff --git a/x-pack/test/api_integration/apis/slos/update_slo.ts b/x-pack/test/api_integration/apis/slos/update_slo.ts
index 7bf6967bd26a3..a8f4aa1a334f8 100644
--- a/x-pack/test/api_integration/apis/slos/update_slo.ts
+++ b/x-pack/test/api_integration/apis/slos/update_slo.ts
@@ -14,7 +14,7 @@ import { loadTestData } from './helper/load_test_data';
import { sloData } from './fixtures/create_slo';
export default function ({ getService }: FtrProviderContext) {
- describe('Update SLOs', function () {
+ describe('UpdateSLOs', function () {
this.tags('skipCloud');
const supertestAPI = getService('supertest');
diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts
index 044e66fe239f7..051ae14396687 100644
--- a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts
+++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts
@@ -10,7 +10,6 @@ import { v4 as uuidv4 } from 'uuid';
import {
ConfigKey,
HTTPFields,
- LocationStatus,
PrivateLocation,
ServiceLocation,
} from '@kbn/synthetics-plugin/common/runtime_types';
@@ -19,6 +18,7 @@ import { formatKibanaNamespace } from '@kbn/synthetics-plugin/common/formatters'
import { omit } from 'lodash';
import { PackagePolicy } from '@kbn/fleet-plugin/common';
import expect from '@kbn/expect';
+import { getDevLocation } from '@kbn/synthetics-plugin/server/synthetics_service/get_service_locations';
import { FtrProviderContext } from '../../ftr_provider_context';
import { getFixtureJson } from './helper/get_fixture_json';
import { comparePolicies, getTestSyntheticsPolicy } from './sample_data/test_policy';
@@ -79,15 +79,7 @@ export default function ({ getService }: FtrProviderContext) {
const apiResponse = await supertestAPI.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS);
const testResponse: Array = [
- {
- id: 'dev',
- label: 'Dev Service',
- geo: { lat: 0, lon: 0 },
- url: 'mockDevUrl',
- isServiceManaged: true,
- status: LocationStatus.EXPERIMENTAL,
- isInvalid: false,
- },
+ ...getDevLocation('mockDevUrl'),
{
id: testFleetPolicyID,
isServiceManaged: false,
diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts
index bd6f94a44f6c1..9654a2bc43404 100644
--- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts
+++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts
@@ -2036,7 +2036,7 @@ export default function ({ getService }: FtrProviderContext) {
failedMonitors: [
{
details:
- "Invalid locations specified. Elastic managed Location(s) 'does not exist' not found. Available locations are 'dev'",
+ "Invalid locations specified. Elastic managed Location(s) 'does not exist' not found. Available locations are 'dev|dev2'",
id: httpProjectMonitors.monitors[1].id,
payload: {
'check.request': {
diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_public_api.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_public_api.ts
index 5da370d1c634f..082d1aebd6d76 100644
--- a/x-pack/test/api_integration/apis/synthetics/add_monitor_public_api.ts
+++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_public_api.ts
@@ -44,7 +44,7 @@ export default function ({ getService }: FtrProviderContext) {
it('return error if invalid location specified', async () => {
const { message } = await addMonitorAPI({ type: 'http', locations: ['mars'] }, 400);
expect(message).eql(
- "Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev'"
+ "Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev|dev2'"
);
});
@@ -68,7 +68,7 @@ export default function ({ getService }: FtrProviderContext) {
400
);
expect(result.message).eql(
- "Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev' Private Location(s) 'moon' not found. No private location available to use."
+ "Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev|dev2' Private Location(s) 'moon' not found. No private location available to use."
);
});
diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts
index fa7c780a2d971..e52d83ce9b263 100644
--- a/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts
+++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts
@@ -115,7 +115,7 @@ export default function ({ getService }: FtrProviderContext) {
400
);
expect(message).eql(
- "Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev'"
+ "Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev|dev2'"
);
});
@@ -141,7 +141,7 @@ export default function ({ getService }: FtrProviderContext) {
400
);
expect(result.message).eql(
- "Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev' Private Location(s) 'moon' not found. No private location available to use."
+ "Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev|dev2' Private Location(s) 'moon' not found. No private location available to use."
);
});
diff --git a/x-pack/test/api_integration/apis/synthetics/suggestions.ts b/x-pack/test/api_integration/apis/synthetics/suggestions.ts
index dab822f832c08..043c1c4da0ee6 100644
--- a/x-pack/test/api_integration/apis/synthetics/suggestions.ts
+++ b/x-pack/test/api_integration/apis/synthetics/suggestions.ts
@@ -154,13 +154,6 @@ export default function ({ getService }: FtrProviderContext) {
value: expect.any(String),
})),
]),
- projects: [
- {
- count: 2,
- label: project,
- value: project,
- },
- ],
monitorTypes: [
{
count: 20,
@@ -173,6 +166,13 @@ export default function ({ getService }: FtrProviderContext) {
value: 'icmp',
},
],
+ projects: [
+ {
+ count: 2,
+ label: project,
+ value: project,
+ },
+ ],
tags: expect.arrayContaining([
{
count: 21,
@@ -242,23 +242,18 @@ export default function ({ getService }: FtrProviderContext) {
value: expect.any(String),
}))
),
- projects: [
+ monitorTypes: [
{
count: 2,
- label: project,
- value: project,
+ label: 'icmp',
+ value: 'icmp',
},
],
- monitorTypes: [
- // {
- // count: 20,
- // label: 'http',
- // value: 'http',
- // },
+ projects: [
{
count: 2,
- label: 'icmp',
- value: 'icmp',
+ label: project,
+ value: project,
},
],
tags: expect.arrayContaining([
diff --git a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts
index c262cc3c79bdc..8e0aaeff21580 100644
--- a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts
+++ b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts
@@ -82,6 +82,15 @@ export default function ({ getService }: FtrProviderContext) {
status: LocationStatus.EXPERIMENTAL,
isInvalid: false,
},
+ {
+ id: 'dev2',
+ label: 'Dev Service 2',
+ geo: { lat: 0, lon: 0 },
+ url: 'mockDevUrl',
+ isServiceManaged: true,
+ status: LocationStatus.EXPERIMENTAL,
+ isInvalid: false,
+ },
{
id: testFleetPolicyID,
isInvalid: false,
diff --git a/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts b/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts
index 1b1497140875e..2065d1307fbda 100644
--- a/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts
+++ b/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts
@@ -73,6 +73,62 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);
});
+ it(`should show setup technology selector in edit mode`, async () => {
+ const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`;
+ await cisIntegration.navigateToAddIntegrationCspmWithVersionPage(
+ CLOUD_CREDENTIALS_PACKAGE_VERSION
+ );
+
+ await cisIntegration.clickOptionButton(CIS_AWS_OPTION_TEST_ID);
+ await cisIntegration.clickOptionButton(AWS_SINGLE_ACCOUNT_TEST_ID);
+
+ await cisIntegration.inputIntegrationName(integrationPolicyName);
+
+ await cisIntegration.selectSetupTechnology('agentless');
+ await cisIntegration.selectAwsCredentials('direct');
+
+ await pageObjects.header.waitUntilLoadingHasFinished();
+
+ await cisIntegration.clickSaveButton();
+ await pageObjects.header.waitUntilLoadingHasFinished();
+
+ expect(await cisIntegrationAws.showPostInstallCloudFormationModal()).to.be(false);
+
+ await cisIntegration.navigateToIntegrationCspList();
+ await pageObjects.header.waitUntilLoadingHasFinished();
+
+ await cisIntegration.navigateToEditIntegrationPage();
+ await pageObjects.header.waitUntilLoadingHasFinished();
+
+ expect(await cisIntegration.showSetupTechnologyComponent()).to.be(true);
+ });
+
+ it(`should hide setup technology selector in edit mode`, async () => {
+ const integrationPolicyName = `cloud_security_posture1-${new Date().toISOString()}`;
+ await cisIntegration.navigateToAddIntegrationCspmWithVersionPage(
+ CLOUD_CREDENTIALS_PACKAGE_VERSION
+ );
+
+ await cisIntegration.clickOptionButton(CIS_AWS_OPTION_TEST_ID);
+ await cisIntegration.clickOptionButton(AWS_SINGLE_ACCOUNT_TEST_ID);
+
+ await cisIntegration.inputIntegrationName(integrationPolicyName);
+ await cisIntegration.selectSetupTechnology('agent-based');
+ await pageObjects.header.waitUntilLoadingHasFinished();
+
+ await cisIntegration.clickSaveButton();
+ await pageObjects.header.waitUntilLoadingHasFinished();
+
+ expect(await cisIntegrationAws.showPostInstallCloudFormationModal()).to.be(true);
+
+ await cisIntegration.navigateToIntegrationCspList();
+ await pageObjects.header.waitUntilLoadingHasFinished();
+ await cisIntegration.navigateToEditIntegrationPage();
+ await pageObjects.header.waitUntilLoadingHasFinished();
+
+ expect(await cisIntegration.showSetupTechnologyComponent()).to.be(false);
+ });
+
it(`should create default agent-based agent`, async () => {
const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`;
diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts
index 8732f0ba5b012..e3ef420055196 100644
--- a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts
+++ b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts
@@ -285,6 +285,11 @@ export function AddCisIntegrationFormPageProvider({
);
await agentOption.click();
};
+
+ const showSetupTechnologyComponent = async () => {
+ return await testSubjects.exists(SETUP_TECHNOLOGY_SELECTOR_ACCORDION_TEST_SUBJ);
+ };
+
const selectAwsCredentials = async (credentialType: 'direct' | 'temporary') => {
await clickOptionButton(AWS_CREDENTIAL_SELECTOR);
await selectValue(
@@ -544,5 +549,7 @@ export function AddCisIntegrationFormPageProvider({
getFirstCspmIntegrationPageAgent,
getAgentBasedPolicyValue,
showSuccessfulToast,
+ showSetupTechnologyComponent,
+ navigateToEditIntegrationPage,
};
}
diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts
index ebe7a91019094..fddf71eaf98a1 100644
--- a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts
+++ b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts
@@ -166,7 +166,7 @@ export default function (providerContext: FtrProviderContext) {
.set('kbn-xsrf', 'xxxx')
.expect(200);
- await supertest.get(`/api/fleet/agent_policies/${agentPolicy.id}`).expect(404);
+ await supertest.get(`/api/fleet/agent_policies/${agentPolicy.id}`).expect(200);
});
});
describe('Delete bulk', () => {
diff --git a/x-pack/test/functional/apps/lens/group4/index.ts b/x-pack/test/functional/apps/lens/group4/index.ts
index 51b3bac4c519f..61074d6aa6d78 100644
--- a/x-pack/test/functional/apps/lens/group4/index.ts
+++ b/x-pack/test/functional/apps/lens/group4/index.ts
@@ -82,5 +82,6 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext
loadTestFile(require.resolve('./share')); // 1m 20s
// keep it last in the group
loadTestFile(require.resolve('./tsdb')); // 1m
+ loadTestFile(require.resolve('./logsdb')); // 1m
});
};
diff --git a/x-pack/test/functional/apps/lens/group4/logsdb.ts b/x-pack/test/functional/apps/lens/group4/logsdb.ts
new file mode 100644
index 0000000000000..a58b5c6bf806f
--- /dev/null
+++ b/x-pack/test/functional/apps/lens/group4/logsdb.ts
@@ -0,0 +1,586 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import moment from 'moment';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import {
+ type ScenarioIndexes,
+ getDataMapping,
+ getDocsGenerator,
+ setupScenarioRunner,
+ TIME_PICKER_FORMAT,
+} from './tsdb_logsdb_helpers';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const { common, lens, discover, header } = getPageObjects([
+ 'common',
+ 'lens',
+ 'discover',
+ 'header',
+ ]);
+ const testSubjects = getService('testSubjects');
+ const find = getService('find');
+ const kibanaServer = getService('kibanaServer');
+ const es = getService('es');
+ const log = getService('log');
+ const dataStreams = getService('dataStreams');
+ const indexPatterns = getService('indexPatterns');
+ const esArchiver = getService('esArchiver');
+ const monacoEditor = getService('monacoEditor');
+ const retry = getService('retry');
+
+ const createDocs = getDocsGenerator(log, es, 'logsdb');
+
+ describe('lens logsdb', function () {
+ const logsdbIndex = 'kibana_sample_data_logslogsdb';
+ const logsdbDataView = logsdbIndex;
+ const logsdbEsArchive = 'test/functional/fixtures/es_archiver/kibana_sample_data_logs_logsdb';
+ const fromTime = 'Apr 16, 2023 @ 00:00:00.000';
+ const toTime = 'Jun 16, 2023 @ 00:00:00.000';
+
+ before(async () => {
+ log.info(`loading ${logsdbIndex} index...`);
+ await esArchiver.loadIfNeeded(logsdbEsArchive);
+ log.info(`creating a data view for "${logsdbDataView}"...`);
+ await indexPatterns.create(
+ {
+ title: logsdbDataView,
+ timeFieldName: '@timestamp',
+ },
+ { override: true }
+ );
+ log.info(`updating settings to use the "${logsdbDataView}" dataView...`);
+ await kibanaServer.uiSettings.update({
+ 'dateFormat:tz': 'UTC',
+ defaultIndex: '0ae0bc7a-e4ca-405c-ab67-f2b5913f2a51',
+ 'timepicker:timeDefaults': `{ "from": "${fromTime}", "to": "${toTime}" }`,
+ });
+ });
+
+ after(async () => {
+ await kibanaServer.savedObjects.cleanStandardList();
+ await kibanaServer.uiSettings.replace({});
+ await es.indices.delete({ index: [logsdbIndex] });
+ });
+
+ describe('smoke testing functions support', () => {
+ before(async () => {
+ await common.navigateToApp('lens');
+ await lens.switchDataPanelIndexPattern(logsdbDataView);
+ await lens.goToTimeRange();
+ });
+
+ afterEach(async () => {
+ await lens.removeLayer();
+ });
+
+ // skip count for now as it's a special function and will
+ // change automatically the unsupported field to Records when detected
+ const allOperations = [
+ 'average',
+ 'max',
+ 'last_value',
+ 'median',
+ 'percentile',
+ 'percentile_rank',
+ 'standard_deviation',
+ 'sum',
+ 'unique_count',
+ 'min',
+ 'max',
+ 'counter_rate',
+ 'last_value',
+ ];
+
+ it(`should work with all operations`, async () => {
+ // start from a count() over a date histogram
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: '@timestamp',
+ });
+
+ // minimum supports all logsdb field types
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'count',
+ field: 'bytes',
+ keepOpen: true,
+ });
+
+ // now check that operations won't show the incompatibility tooltip
+ for (const operation of allOperations) {
+ expect(
+ testSubjects.exists(`lns-indexPatternDimension-${operation} incompatible`, {
+ timeout: 500,
+ })
+ ).to.eql(false);
+ }
+
+ for (const operation of allOperations) {
+ // try to change to the provided function and check all is ok
+ await lens.selectOperation(operation);
+
+ expect(
+ await find.existsByCssSelector(
+ '[data-test-subj="indexPattern-field-selection-row"] .euiFormErrorText'
+ )
+ ).to.be(false);
+ }
+ await lens.closeDimensionEditor();
+ });
+
+ describe('Scenarios with changing stream type', () => {
+ const getScenarios = (
+ initialIndex: string
+ ): Array<{
+ name: string;
+ indexes: ScenarioIndexes[];
+ }> => [
+ {
+ name: 'LogsDB stream with no additional stream/index',
+ indexes: [{ index: initialIndex }],
+ },
+ {
+ name: 'LogsDB stream with no additional stream/index and no host.name field',
+ indexes: [
+ {
+ index: `${initialIndex}_no_host`,
+ removeLogsDBFields: true,
+ create: true,
+ mode: 'logsdb',
+ },
+ ],
+ },
+ {
+ name: 'LogsDB stream with an additional regular index',
+ indexes: [{ index: initialIndex }, { index: 'regular_index', create: true }],
+ },
+ {
+ name: 'LogsDB stream with an additional LogsDB stream',
+ indexes: [
+ { index: initialIndex },
+ { index: 'logsdb_index_2', create: true, mode: 'logsdb' },
+ ],
+ },
+ {
+ name: 'LogsDB stream with an additional TSDB stream',
+ indexes: [{ index: initialIndex }, { index: 'tsdb_index', create: true, mode: 'tsdb' }],
+ },
+ {
+ name: 'LogsDB stream with an additional TSDB stream downsampled',
+ indexes: [
+ { index: initialIndex },
+ { index: 'tsdb_index_downsampled', create: true, mode: 'tsdb', downsample: true },
+ ],
+ },
+ ];
+
+ const { runTestsForEachScenario, toTimeForScenarios, fromTimeForScenarios } =
+ setupScenarioRunner(getService, getPageObjects, getScenarios);
+
+ describe('Data-stream upgraded to LogsDB scenarios', () => {
+ const streamIndex = 'data_stream';
+ // rollover does not allow to change name, it will just change backing index underneath
+ const streamConvertedToLogsDBIndex = streamIndex;
+
+ before(async () => {
+ log.info(`Creating "${streamIndex}" data stream...`);
+ await dataStreams.createDataStream(
+ streamIndex,
+ getDataMapping({ mode: 'logsdb' }),
+ undefined
+ );
+
+ // add some data to the stream
+ await createDocs(streamIndex, { isStream: true }, fromTimeForScenarios);
+
+ log.info(`Update settings for "${streamIndex}" dataView...`);
+ await kibanaServer.uiSettings.update({
+ 'dateFormat:tz': 'UTC',
+ 'timepicker:timeDefaults': '{ "from": "now-1y", "to": "now" }',
+ });
+ log.info(`Upgrade "${streamIndex}" stream to LogsDB...`);
+
+ const logsdbMapping = getDataMapping({ mode: 'logsdb' });
+ await dataStreams.upgradeStream(streamIndex, logsdbMapping, 'logsdb');
+ log.info(
+ `Add more data to new "${streamConvertedToLogsDBIndex}" dataView (now with LogsDB backing index)...`
+ );
+ // add some more data when upgraded
+ await createDocs(streamConvertedToLogsDBIndex, { isStream: true }, toTimeForScenarios);
+ });
+
+ after(async () => {
+ await dataStreams.deleteDataStream(streamIndex);
+ });
+
+ runTestsForEachScenario(streamConvertedToLogsDBIndex, 'logsdb', (indexes) => {
+ it(`should visualize a date histogram chart`, async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: '@timestamp',
+ });
+
+ // check that a basic agg on a field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+
+ await lens.waitForVisualization('xyVisChart');
+ const data = await lens.getCurrentChartDebugState('xyVisChart');
+ const bars = data?.bars![0].bars;
+
+ log.info('Check counter data before the upgrade');
+ // check there's some data before the upgrade
+ expect(bars?.[0].y).to.be.above(0);
+ log.info('Check counter data after the upgrade');
+ // check there's some data after the upgrade
+ expect(bars?.[bars.length - 1].y).to.be.above(0);
+ });
+
+ it(`should visualize a date histogram chart using a different date field`, async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+
+ await lens.waitForVisualization('xyVisChart');
+ const data = await lens.getCurrentChartDebugState('xyVisChart');
+ const bars = data?.bars![0].bars;
+
+ log.info('Check counter data before the upgrade');
+ // check there's some data before the upgrade
+ expect(bars?.[0].y).to.be.above(0);
+ log.info('Check counter data after the upgrade');
+ // check there's some data after the upgrade
+ expect(bars?.[bars.length - 1].y).to.be.above(0);
+ });
+
+ it('should visualize an annotation layer from a logsDB stream', async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+ await lens.createLayer('annotations');
+
+ expect(
+ (await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length
+ ).to.eql(2);
+ expect(
+ await (
+ await testSubjects.find('lnsXY_xAnnotationsPanel > lns-dimensionTrigger')
+ ).getVisibleText()
+ ).to.eql('Event');
+ await testSubjects.click('lnsXY_xAnnotationsPanel > lns-dimensionTrigger');
+ await testSubjects.click('lnsXY_annotation_query');
+ await lens.configureQueryAnnotation({
+ queryString: 'host.name: *',
+ timeField: '@timestamp',
+ textDecoration: { type: 'name' },
+ extraFields: ['host.name', 'utc_time'],
+ });
+ await lens.closeDimensionEditor();
+
+ await testSubjects.existOrFail('xyVisGroupedAnnotationIcon');
+ await lens.removeLayer(1);
+ });
+
+ it('should visualize an annotation layer from a logsDB stream using another time field', async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+ await lens.createLayer('annotations');
+
+ expect(
+ (await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length
+ ).to.eql(2);
+ expect(
+ await (
+ await testSubjects.find('lnsXY_xAnnotationsPanel > lns-dimensionTrigger')
+ ).getVisibleText()
+ ).to.eql('Event');
+ await testSubjects.click('lnsXY_xAnnotationsPanel > lns-dimensionTrigger');
+ await testSubjects.click('lnsXY_annotation_query');
+ await lens.configureQueryAnnotation({
+ queryString: 'host.name: *',
+ timeField: 'utc_time',
+ textDecoration: { type: 'name' },
+ extraFields: ['host.name', '@timestamp'],
+ });
+ await lens.closeDimensionEditor();
+
+ await testSubjects.existOrFail('xyVisGroupedAnnotationIcon');
+ await lens.removeLayer(1);
+ });
+
+ it('should visualize correctly ES|QL queries based on a LogsDB stream', async () => {
+ await common.navigateToApp('discover');
+ await discover.selectTextBaseLang();
+ await header.waitUntilLoadingHasFinished();
+ await monacoEditor.setCodeEditorValue(
+ `from ${indexes
+ .map(({ index }) => index)
+ .join(', ')} | stats averageB = avg(bytes) by extension`
+ );
+ await testSubjects.click('querySubmitButton');
+ await header.waitUntilLoadingHasFinished();
+ await testSubjects.click('unifiedHistogramEditFlyoutVisualization');
+
+ await header.waitUntilLoadingHasFinished();
+
+ await retry.waitFor('lens flyout', async () => {
+ const dimensions = await testSubjects.findAll('lns-dimensionTrigger-textBased');
+ return (
+ dimensions.length === 2 && (await dimensions[1].getVisibleText()) === 'averageB'
+ );
+ });
+
+ // go back to Lens to not break the wrapping function
+ await common.navigateToApp('lens');
+ });
+ });
+ });
+
+ describe('LogsDB downgraded to regular data stream scenarios', () => {
+ const logsdbStream = 'logsdb_stream_dowgradable';
+ // rollover does not allow to change name, it will just change backing index underneath
+ const logsdbConvertedToStream = logsdbStream;
+
+ before(async () => {
+ log.info(`Creating "${logsdbStream}" data stream...`);
+ await dataStreams.createDataStream(
+ logsdbStream,
+ getDataMapping({ mode: 'logsdb' }),
+ 'logsdb'
+ );
+
+ // add some data to the stream
+ await createDocs(logsdbStream, { isStream: true }, fromTimeForScenarios);
+
+ log.info(`Update settings for "${logsdbStream}" dataView...`);
+ await kibanaServer.uiSettings.update({
+ 'dateFormat:tz': 'UTC',
+ 'timepicker:timeDefaults': '{ "from": "now-1y", "to": "now" }',
+ });
+ log.info(
+ `Dowgrade "${logsdbStream}" stream into regular stream "${logsdbConvertedToStream}"...`
+ );
+
+ await dataStreams.downgradeStream(
+ logsdbStream,
+ getDataMapping({ mode: 'logsdb' }),
+ 'logsdb'
+ );
+ log.info(
+ `Add more data to new "${logsdbConvertedToStream}" dataView (no longer LogsDB)...`
+ );
+ // add some more data when upgraded
+ await createDocs(logsdbConvertedToStream, { isStream: true }, toTimeForScenarios);
+ });
+
+ after(async () => {
+ await dataStreams.deleteDataStream(logsdbConvertedToStream);
+ });
+
+ runTestsForEachScenario(logsdbConvertedToStream, 'logsdb', (indexes) => {
+ it(`should visualize a date histogram chart`, async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: '@timestamp',
+ });
+
+ // check that a basic agg on a field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+
+ await lens.waitForVisualization('xyVisChart');
+ const data = await lens.getCurrentChartDebugState('xyVisChart');
+ const bars = data?.bars![0].bars;
+
+ log.info('Check counter data before the upgrade');
+ // check there's some data before the upgrade
+ expect(bars?.[0].y).to.be.above(0);
+ log.info('Check counter data after the upgrade');
+ // check there's some data after the upgrade
+ expect(bars?.[bars.length - 1].y).to.be.above(0);
+ });
+
+ it(`should visualize a date histogram chart using a different date field`, async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+
+ await lens.waitForVisualization('xyVisChart');
+ const data = await lens.getCurrentChartDebugState('xyVisChart');
+ const bars = data?.bars![0].bars;
+
+ log.info('Check counter data before the upgrade');
+ // check there's some data before the upgrade
+ expect(bars?.[0].y).to.be.above(0);
+ log.info('Check counter data after the upgrade');
+ // check there's some data after the upgrade
+ expect(bars?.[bars.length - 1].y).to.be.above(0);
+ });
+
+ it('should visualize an annotation layer from a logsDB stream', async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+ await lens.createLayer('annotations');
+
+ expect(
+ (await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length
+ ).to.eql(2);
+ expect(
+ await (
+ await testSubjects.find('lnsXY_xAnnotationsPanel > lns-dimensionTrigger')
+ ).getVisibleText()
+ ).to.eql('Event');
+ await testSubjects.click('lnsXY_xAnnotationsPanel > lns-dimensionTrigger');
+ await testSubjects.click('lnsXY_annotation_query');
+ await lens.configureQueryAnnotation({
+ queryString: 'host.name: *',
+ timeField: '@timestamp',
+ textDecoration: { type: 'name' },
+ extraFields: ['host.name', 'utc_time'],
+ });
+ await lens.closeDimensionEditor();
+
+ await testSubjects.existOrFail('xyVisGroupedAnnotationIcon');
+ await lens.removeLayer(1);
+ });
+
+ it('should visualize an annotation layer from a logsDB stream using another time field', async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+ await lens.createLayer('annotations');
+
+ expect(
+ (await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length
+ ).to.eql(2);
+ expect(
+ await (
+ await testSubjects.find('lnsXY_xAnnotationsPanel > lns-dimensionTrigger')
+ ).getVisibleText()
+ ).to.eql('Event');
+ await testSubjects.click('lnsXY_xAnnotationsPanel > lns-dimensionTrigger');
+ await testSubjects.click('lnsXY_annotation_query');
+ await lens.configureQueryAnnotation({
+ queryString: 'host.name: *',
+ timeField: 'utc_time',
+ textDecoration: { type: 'name' },
+ extraFields: ['host.name', '@timestamp'],
+ });
+ await lens.closeDimensionEditor();
+
+ await testSubjects.existOrFail('xyVisGroupedAnnotationIcon');
+ await lens.removeLayer(1);
+ });
+
+ it('should visualize correctly ES|QL queries based on a LogsDB stream', async () => {
+ await common.navigateToApp('discover');
+ await discover.selectTextBaseLang();
+
+ // Use the lens page object here also for discover: both use the same timePicker object
+ await lens.goToTimeRange(
+ fromTimeForScenarios,
+ moment
+ .utc(toTimeForScenarios, TIME_PICKER_FORMAT)
+ .add(2, 'hour')
+ .format(TIME_PICKER_FORMAT)
+ );
+
+ await header.waitUntilLoadingHasFinished();
+ await monacoEditor.setCodeEditorValue(
+ `from ${indexes
+ .map(({ index }) => index)
+ .join(', ')} | stats averageB = avg(bytes) by extension`
+ );
+ await testSubjects.click('querySubmitButton');
+ await header.waitUntilLoadingHasFinished();
+ await testSubjects.click('unifiedHistogramEditFlyoutVisualization');
+
+ await header.waitUntilLoadingHasFinished();
+
+ await retry.waitFor('lens flyout', async () => {
+ const dimensions = await testSubjects.findAll('lns-dimensionTrigger-textBased');
+ return (
+ dimensions.length === 2 && (await dimensions[1].getVisibleText()) === 'averageB'
+ );
+ });
+
+ // go back to Lens to not break the wrapping function
+ await common.navigateToApp('lens');
+ });
+ });
+ });
+ });
+ });
+ });
+}
diff --git a/x-pack/test/functional/apps/lens/group4/tsdb.ts b/x-pack/test/functional/apps/lens/group4/tsdb.ts
index bbe1eef8a442c..3a6aac5ffa39b 100644
--- a/x-pack/test/functional/apps/lens/group4/tsdb.ts
+++ b/x-pack/test/functional/apps/lens/group4/tsdb.ts
@@ -8,234 +8,16 @@
import expect from '@kbn/expect';
import { partition } from 'lodash';
import moment from 'moment';
-import { MappingProperty } from '@elastic/elasticsearch/lib/api/types';
import { FtrProviderContext } from '../../../ftr_provider_context';
-
-const TEST_DOC_COUNT = 100;
-const TIME_PICKER_FORMAT = 'MMM D, YYYY [@] HH:mm:ss.SSS';
-const timeSeriesMetrics: Record = {
- bytes_gauge: 'gauge',
- bytes_counter: 'counter',
-};
-const timeSeriesDimensions = ['request', 'url'];
-
-type TestDoc = Record>;
-
-const testDocTemplate: TestDoc = {
- agent: 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1',
- bytes: 6219,
- clientip: '223.87.60.27',
- extension: 'deb',
- geo: {
- srcdest: 'US:US',
- src: 'US',
- dest: 'US',
- coordinates: { lat: 39.41042861, lon: -88.8454325 },
- },
- host: 'artifacts.elastic.co',
- index: 'kibana_sample_data_logs',
- ip: '223.87.60.27',
- machine: { ram: 8589934592, os: 'win 8' },
- memory: null,
- message:
- '223.87.60.27 - - [2018-07-22T00:39:02.912Z] "GET /elasticsearch/elasticsearch-6.3.2.deb_1 HTTP/1.1" 200 6219 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1"',
- phpmemory: null,
- referer: 'http://twitter.com/success/wendy-lawrence',
- request: '/elasticsearch/elasticsearch-6.3.2.deb',
- response: 200,
- tags: ['success', 'info'],
- '@timestamp': '2018-07-22T00:39:02.912Z',
- url: 'https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.3.2.deb_1',
- utc_time: '2018-07-22T00:39:02.912Z',
- event: { dataset: 'sample_web_logs' },
- bytes_gauge: 0,
- bytes_counter: 0,
-};
-
-function getDataMapping(
- { tsdb, removeTSDBFields }: { tsdb: boolean; removeTSDBFields?: boolean } = {
- tsdb: false,
- }
-): Record {
- const dataStreamMapping: Record = {
- '@timestamp': {
- type: 'date',
- },
- agent: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- bytes: {
- type: 'long',
- },
- bytes_counter: {
- type: 'long',
- },
- bytes_gauge: {
- type: 'long',
- },
- clientip: {
- type: 'ip',
- },
- event: {
- properties: {
- dataset: {
- type: 'keyword',
- },
- },
- },
- extension: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- geo: {
- properties: {
- coordinates: {
- type: 'geo_point',
- },
- dest: {
- type: 'keyword',
- },
- src: {
- type: 'keyword',
- },
- srcdest: {
- type: 'keyword',
- },
- },
- },
- host: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- index: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- ip: {
- type: 'ip',
- },
- machine: {
- properties: {
- os: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- ram: {
- type: 'long',
- },
- },
- },
- memory: {
- type: 'double',
- },
- message: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- phpmemory: {
- type: 'long',
- },
- referer: {
- type: 'keyword',
- },
- request: {
- type: 'keyword',
- },
- response: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- tags: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- timestamp: {
- path: '@timestamp',
- type: 'alias',
- },
- url: {
- type: 'keyword',
- },
- utc_time: {
- type: 'date',
- },
- };
-
- if (tsdb) {
- // augment the current mapping
- for (const [fieldName, fieldMapping] of Object.entries(dataStreamMapping || {})) {
- if (
- timeSeriesMetrics[fieldName] &&
- (fieldMapping.type === 'double' || fieldMapping.type === 'long')
- ) {
- fieldMapping.time_series_metric = timeSeriesMetrics[fieldName];
- }
-
- if (timeSeriesDimensions.includes(fieldName) && fieldMapping.type === 'keyword') {
- fieldMapping.time_series_dimension = true;
- }
- }
- } else if (removeTSDBFields) {
- for (const fieldName of Object.keys(timeSeriesMetrics)) {
- delete dataStreamMapping[fieldName];
- }
- }
- return dataStreamMapping;
-}
-
-function sumFirstNValues(n: number, bars: Array<{ y: number }> | undefined): number {
- const indexes = Array(n)
- .fill(1)
- .map((_, i) => i);
- let countSum = 0;
- for (const index of indexes) {
- if (bars?.[index]) {
- countSum += bars[index].y;
- }
- }
- return countSum;
-}
+import {
+ type ScenarioIndexes,
+ TEST_DOC_COUNT,
+ TIME_PICKER_FORMAT,
+ getDataMapping,
+ getDocsGenerator,
+ setupScenarioRunner,
+ sumFirstNValues,
+} from './tsdb_logsdb_helpers';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const { common, lens, dashboard } = getPageObjects(['common', 'lens', 'dashboard']);
@@ -245,71 +27,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const es = getService('es');
const log = getService('log');
const dataStreams = getService('dataStreams');
- const elasticChart = getService('elasticChart');
const indexPatterns = getService('indexPatterns');
const esArchiver = getService('esArchiver');
const comboBox = getService('comboBox');
- const createDocs = async (
- esIndex: string,
- { isStream, removeTSDBFields }: { isStream: boolean; removeTSDBFields?: boolean },
- startTime: string
- ) => {
- log.info(
- `Adding ${TEST_DOC_COUNT} to ${esIndex} with starting time from ${moment
- .utc(startTime, TIME_PICKER_FORMAT)
- .format(TIME_PICKER_FORMAT)} to ${moment
- .utc(startTime, TIME_PICKER_FORMAT)
- .add(2 * TEST_DOC_COUNT, 'seconds')
- .format(TIME_PICKER_FORMAT)}`
- );
- const docs = Array(TEST_DOC_COUNT)
- .fill(testDocTemplate)
- .map((templateDoc, i) => {
- const timestamp = moment
- .utc(startTime, TIME_PICKER_FORMAT)
- .add(TEST_DOC_COUNT + i, 'seconds')
- .format();
- const doc: TestDoc = {
- ...templateDoc,
- '@timestamp': timestamp,
- utc_time: timestamp,
- bytes_gauge: Math.floor(Math.random() * 10000 * i),
- bytes_counter: 5000,
- };
- if (removeTSDBFields) {
- for (const field of Object.keys(timeSeriesMetrics)) {
- delete doc[field];
- }
- }
- return doc;
- });
-
- const result = await es.bulk(
- {
- index: esIndex,
- body: docs.map((d) => `{"${isStream ? 'create' : 'index'}": {}}\n${JSON.stringify(d)}\n`),
- },
- { meta: true }
- );
-
- const res = result.body;
-
- if (res.errors) {
- const resultsWithErrors = res.items
- .filter(({ index }) => index?.error)
- .map(({ index }) => index?.error);
- for (const error of resultsWithErrors) {
- log.error(`Error: ${JSON.stringify(error)}`);
- }
- const [indexExists, dataStreamExists] = await Promise.all([
- es.indices.exists({ index: esIndex }),
- es.indices.getDataStream({ name: esIndex }),
- ]);
- log.debug(`Index exists: ${indexExists} - Data stream exists: ${dataStreamExists}`);
- }
- log.info(`Indexed ${res.items.length} test data docs.`);
- };
+ const createDocs = getDocsGenerator(log, es, 'tsdb');
describe('lens tsdb', function () {
const tsdbIndex = 'kibana_sample_data_logstsdb';
@@ -592,23 +314,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
describe('Scenarios with changing stream type', () => {
- const now = moment().utc();
- const fromMoment = now.clone().subtract(1, 'hour');
- const toMoment = now.clone();
- const fromTimeForScenarios = fromMoment.format(TIME_PICKER_FORMAT);
- const toTimeForScenarios = toMoment.format(TIME_PICKER_FORMAT);
-
const getScenarios = (
initialIndex: string
): Array<{
name: string;
- indexes: Array<{
- index: string;
- create?: boolean;
- downsample?: boolean;
- tsdb?: boolean;
- removeTSDBFields?: boolean;
- }>;
+ indexes: ScenarioIndexes[];
}> => [
{
name: 'Dataview with no additional stream/index',
@@ -625,7 +335,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
name: 'Dataview with an additional downsampled TSDB stream',
indexes: [
{ index: initialIndex },
- { index: 'tsdb_index_2', create: true, tsdb: true, downsample: true },
+ { index: 'tsdb_index_2', create: true, mode: 'tsdb', downsample: true },
],
},
{
@@ -633,112 +343,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
indexes: [
{ index: initialIndex },
{ index: 'regular_index', create: true, removeTSDBFields: true },
- { index: 'tsdb_index_2', create: true, tsdb: true, downsample: true },
+ { index: 'tsdb_index_2', create: true, mode: 'tsdb', downsample: true },
],
},
{
name: 'Dataview with an additional TSDB stream',
- indexes: [{ index: initialIndex }, { index: 'tsdb_index_2', create: true, tsdb: true }],
+ indexes: [{ index: initialIndex }, { index: 'tsdb_index_2', create: true, mode: 'tsdb' }],
},
];
- function runTestsForEachScenario(
- initialIndex: string,
- testingFn: (
- indexes: Array<{
- index: string;
- create?: boolean;
- downsample?: boolean;
- tsdb?: boolean;
- removeTSDBFields?: boolean;
- }>
- ) => void
- ): void {
- for (const { name, indexes } of getScenarios(initialIndex)) {
- describe(name, () => {
- let dataViewName: string;
- let downsampledTargetIndex: string = '';
-
- before(async () => {
- for (const { index, create, downsample, tsdb, removeTSDBFields } of indexes) {
- if (create) {
- if (tsdb) {
- await dataStreams.createDataStream(
- index,
- getDataMapping({ tsdb, removeTSDBFields }),
- tsdb
- );
- } else {
- log.info(`creating a index "${index}" with mapping...`);
- await es.indices.create({
- index,
- mappings: {
- properties: getDataMapping({ tsdb: Boolean(tsdb), removeTSDBFields }),
- },
- });
- }
- // add data to the newly created index
- await createDocs(
- index,
- { isStream: Boolean(tsdb), removeTSDBFields },
- fromTimeForScenarios
- );
- }
- if (downsample) {
- downsampledTargetIndex = await dataStreams.downsampleTSDBIndex(index, {
- isStream: Boolean(tsdb),
- });
- }
- }
- dataViewName = `${indexes.map(({ index }) => index).join(',')}${
- downsampledTargetIndex ? `,${downsampledTargetIndex}` : ''
- }`;
- log.info(`creating a data view for "${dataViewName}"...`);
- await indexPatterns.create(
- {
- title: dataViewName,
- timeFieldName: '@timestamp',
- },
- { override: true }
- );
- await common.navigateToApp('lens');
- await elasticChart.setNewChartUiDebugFlag(true);
- // go to the
- await lens.goToTimeRange(
- fromTimeForScenarios,
- moment
- .utc(toTimeForScenarios, TIME_PICKER_FORMAT)
- .add(2, 'hour')
- .format(TIME_PICKER_FORMAT) // consider also new documents
- );
- });
-
- after(async () => {
- for (const { index, create, tsdb } of indexes) {
- if (create) {
- if (tsdb) {
- await dataStreams.deleteDataStream(index);
- } else {
- log.info(`deleting the index "${index}"...`);
- await es.indices.delete({
- index,
- });
- }
- }
- // no need to cleant he specific downsample index as everything linked to the stream
- // is cleaned up automatically
- }
- });
-
- beforeEach(async () => {
- await lens.switchDataPanelIndexPattern(dataViewName);
- await lens.removeLayer();
- });
-
- testingFn(indexes);
- });
- }
- }
+ const { runTestsForEachScenario, toTimeForScenarios, fromTimeForScenarios } =
+ setupScenarioRunner(getService, getPageObjects, getScenarios);
describe('Data-stream upgraded to TSDB scenarios', () => {
const streamIndex = 'data_stream';
@@ -747,7 +362,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
before(async () => {
log.info(`Creating "${streamIndex}" data stream...`);
- await dataStreams.createDataStream(streamIndex, getDataMapping(), false);
+ await dataStreams.createDataStream(
+ streamIndex,
+ getDataMapping({ mode: 'tsdb' }),
+ undefined
+ );
// add some data to the stream
await createDocs(streamIndex, { isStream: true }, fromTimeForScenarios);
@@ -759,8 +378,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
log.info(`Upgrade "${streamIndex}" stream to TSDB...`);
- const tsdbMapping = getDataMapping({ tsdb: true });
- await dataStreams.upgradeStreamToTSDB(streamIndex, tsdbMapping);
+ const tsdbMapping = getDataMapping({ mode: 'tsdb' });
+ await dataStreams.upgradeStream(streamIndex, tsdbMapping, 'tsdb');
log.info(
`Add more data to new "${streamConvertedToTsdbIndex}" dataView (now with TSDB backing index)...`
);
@@ -772,7 +391,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dataStreams.deleteDataStream(streamIndex);
});
- runTestsForEachScenario(streamConvertedToTsdbIndex, (indexes) => {
+ runTestsForEachScenario(streamConvertedToTsdbIndex, 'tsdb', (indexes) => {
it('should detect the data stream has now been upgraded to TSDB', async () => {
await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
@@ -850,7 +469,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
before(async () => {
log.info(`Creating "${tsdbStream}" data stream...`);
- await dataStreams.createDataStream(tsdbStream, getDataMapping({ tsdb: true }), true);
+ await dataStreams.createDataStream(tsdbStream, getDataMapping({ mode: 'tsdb' }), 'tsdb');
// add some data to the stream
await createDocs(tsdbStream, { isStream: true }, fromTimeForScenarios);
@@ -864,7 +483,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
`Dowgrade "${tsdbStream}" stream into regular stream "${tsdbConvertedToStream}"...`
);
- await dataStreams.downgradeTSDBtoStream(tsdbStream, getDataMapping({ tsdb: true }));
+ await dataStreams.downgradeStream(tsdbStream, getDataMapping({ mode: 'tsdb' }), 'tsdb');
log.info(`Add more data to new "${tsdbConvertedToStream}" dataView (no longer TSDB)...`);
// add some more data when upgraded
await createDocs(tsdbConvertedToStream, { isStream: true }, toTimeForScenarios);
@@ -874,7 +493,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dataStreams.deleteDataStream(tsdbConvertedToStream);
});
- runTestsForEachScenario(tsdbConvertedToStream, (indexes) => {
+ runTestsForEachScenario(tsdbConvertedToStream, 'tsdb', (indexes) => {
it('should keep TSDB restrictions only if a tsdb stream is in the dataView mix', async () => {
await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
@@ -893,7 +512,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
testSubjects.exists(`lns-indexPatternDimension-average incompatible`, {
timeout: 500,
})
- ).to.eql(indexes.some(({ tsdb }) => tsdb));
+ ).to.eql(indexes.some(({ mode }) => mode === 'tsdb'));
await lens.closeDimensionEditor();
});
diff --git a/x-pack/test/functional/apps/lens/group4/tsdb_logsdb_helpers.ts b/x-pack/test/functional/apps/lens/group4/tsdb_logsdb_helpers.ts
new file mode 100644
index 0000000000000..e0169ebbae575
--- /dev/null
+++ b/x-pack/test/functional/apps/lens/group4/tsdb_logsdb_helpers.ts
@@ -0,0 +1,480 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import expect from '@kbn/expect';
+import { Client } from '@elastic/elasticsearch';
+import { MappingProperty } from '@elastic/elasticsearch/lib/api/types';
+import { ToolingLog } from '@kbn/tooling-log';
+import moment from 'moment';
+import type { FtrProviderContext } from '../../../ftr_provider_context';
+
+export const TEST_DOC_COUNT = 100;
+export const TIME_PICKER_FORMAT = 'MMM D, YYYY [@] HH:mm:ss.SSS';
+export const timeSeriesMetrics: Record = {
+ bytes_gauge: 'gauge',
+ bytes_counter: 'counter',
+};
+export const timeSeriesDimensions = ['request', 'url'];
+export const logsDBSpecialFields = ['host'];
+
+export const sharedESArchive =
+ 'test/functional/fixtures/es_archiver/kibana_sample_data_logs_logsdb';
+export const fromTime = 'Apr 16, 2023 @ 00:00:00.000';
+export const toTime = 'Jun 16, 2023 @ 00:00:00.000';
+
+export type TestDoc = Record>;
+
+export function testDocTemplate(mode: 'tsdb' | 'logsdb'): TestDoc {
+ return {
+ agent: 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1',
+ bytes: 6219,
+ clientip: '223.87.60.27',
+ extension: 'deb',
+ geo: {
+ srcdest: 'US:US',
+ src: 'US',
+ dest: 'US',
+ coordinates: { lat: 39.41042861, lon: -88.8454325 },
+ },
+ host: mode === 'tsdb' ? 'artifacts.elastic.co' : { name: 'artifacts.elastic.co' },
+ index: 'kibana_sample_data_logs',
+ ip: '223.87.60.27',
+ machine: { ram: 8589934592, os: 'win 8' },
+ memory: null,
+ message:
+ '223.87.60.27 - - [2018-07-22T00:39:02.912Z] "GET /elasticsearch/elasticsearch-6.3.2.deb_1 HTTP/1.1" 200 6219 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1"',
+ phpmemory: null,
+ referer: 'http://twitter.com/success/wendy-lawrence',
+ request: '/elasticsearch/elasticsearch-6.3.2.deb',
+ response: 200,
+ tags: ['success', 'info'],
+ '@timestamp': '2018-07-22T00:39:02.912Z',
+ url: 'https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.3.2.deb_1',
+ utc_time: '2018-07-22T00:39:02.912Z',
+ event: { dataset: 'sample_web_logs' },
+ bytes_gauge: 0,
+ bytes_counter: 0,
+ };
+}
+
+export function getDataMapping({
+ mode,
+ removeTSDBFields,
+ removeLogsDBFields,
+}: {
+ mode: 'tsdb' | 'logsdb';
+ removeTSDBFields?: boolean;
+ removeLogsDBFields?: boolean;
+}): Record {
+ const dataStreamMapping: Record = {
+ '@timestamp': {
+ type: 'date',
+ },
+ agent: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ bytes: {
+ type: 'long',
+ },
+ bytes_counter: {
+ type: 'long',
+ },
+ bytes_gauge: {
+ type: 'long',
+ },
+ clientip: {
+ type: 'ip',
+ },
+ event: {
+ properties: {
+ dataset: {
+ type: 'keyword',
+ },
+ },
+ },
+ extension: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ geo: {
+ properties: {
+ coordinates: {
+ type: 'geo_point',
+ },
+ dest: {
+ type: 'keyword',
+ },
+ src: {
+ type: 'keyword',
+ },
+ srcdest: {
+ type: 'keyword',
+ },
+ },
+ },
+ host:
+ mode === 'tsdb'
+ ? {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ }
+ : {
+ properties: {
+ name: {
+ type: 'keyword',
+ },
+ },
+ },
+ index: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ ip: {
+ type: 'ip',
+ },
+ machine: {
+ properties: {
+ os: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ ram: {
+ type: 'long',
+ },
+ },
+ },
+ memory: {
+ type: 'double',
+ },
+ message: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ phpmemory: {
+ type: 'long',
+ },
+ referer: {
+ type: 'keyword',
+ },
+ request: {
+ type: 'keyword',
+ },
+ response: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ tags: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ timestamp: {
+ path: '@timestamp',
+ type: 'alias',
+ },
+ url: {
+ type: 'keyword',
+ },
+ utc_time: {
+ type: 'date',
+ },
+ };
+
+ if (mode === 'tsdb') {
+ // augment the current mapping
+ for (const [fieldName, fieldMapping] of Object.entries(dataStreamMapping || {})) {
+ if (
+ timeSeriesMetrics[fieldName] &&
+ (fieldMapping.type === 'double' || fieldMapping.type === 'long')
+ ) {
+ fieldMapping.time_series_metric = timeSeriesMetrics[fieldName];
+ }
+
+ if (timeSeriesDimensions.includes(fieldName) && fieldMapping.type === 'keyword') {
+ fieldMapping.time_series_dimension = true;
+ }
+ }
+ }
+ if (removeTSDBFields) {
+ for (const fieldName of Object.keys(timeSeriesMetrics)) {
+ delete dataStreamMapping[fieldName];
+ }
+ }
+ if (removeLogsDBFields) {
+ for (const fieldName of logsDBSpecialFields) {
+ delete dataStreamMapping[fieldName];
+ }
+ }
+ return dataStreamMapping;
+}
+
+export function sumFirstNValues(n: number, bars: Array<{ y: number }> | undefined): number {
+ const indexes = Array(n)
+ .fill(1)
+ .map((_, i) => i);
+ let countSum = 0;
+ for (const index of indexes) {
+ if (bars?.[index]) {
+ countSum += bars[index].y;
+ }
+ }
+ return countSum;
+}
+
+export const getDocsGenerator =
+ (log: ToolingLog, es: Client, mode: 'tsdb' | 'logsdb') =>
+ async (
+ esIndex: string,
+ {
+ isStream,
+ removeTSDBFields,
+ removeLogsDBFields,
+ }: { isStream: boolean; removeTSDBFields?: boolean; removeLogsDBFields?: boolean },
+ startTime: string
+ ) => {
+ log.info(
+ `Adding ${TEST_DOC_COUNT} to ${esIndex} with starting time from ${moment
+ .utc(startTime, TIME_PICKER_FORMAT)
+ .format(TIME_PICKER_FORMAT)} to ${moment
+ .utc(startTime, TIME_PICKER_FORMAT)
+ .add(2 * TEST_DOC_COUNT, 'seconds')
+ .format(TIME_PICKER_FORMAT)}`
+ );
+ const docs = Array(TEST_DOC_COUNT)
+ .fill(testDocTemplate(mode))
+ .map((templateDoc, i) => {
+ const timestamp = moment
+ .utc(startTime, TIME_PICKER_FORMAT)
+ .add(TEST_DOC_COUNT + i, 'seconds')
+ .format();
+ const doc: TestDoc = {
+ ...templateDoc,
+ '@timestamp': timestamp,
+ utc_time: timestamp,
+ bytes_gauge: Math.floor(Math.random() * 10000 * i),
+ bytes_counter: 5000,
+ };
+ if (removeTSDBFields) {
+ for (const field of Object.keys(timeSeriesMetrics)) {
+ delete doc[field];
+ }
+ }
+ // do not remove the fields for logsdb - ignore the flag
+ return doc;
+ });
+
+ const result = await es.bulk(
+ {
+ index: esIndex,
+ body: docs.map((d) => `{"${isStream ? 'create' : 'index'}": {}}\n${JSON.stringify(d)}\n`),
+ },
+ { meta: true }
+ );
+
+ const res = result.body;
+
+ if (res.errors) {
+ const resultsWithErrors = res.items
+ .filter(({ index }) => index?.error)
+ .map(({ index }) => index?.error);
+ for (const error of resultsWithErrors) {
+ log.error(`Error: ${JSON.stringify(error)}`);
+ }
+ const [indexExists, dataStreamExists] = await Promise.all([
+ es.indices.exists({ index: esIndex }),
+ es.indices.getDataStream({ name: esIndex }),
+ ]);
+ log.debug(`Index exists: ${indexExists} - Data stream exists: ${dataStreamExists}`);
+ }
+ log.info(`Indexed ${res.items.length} test data docs.`);
+ };
+
+export interface ScenarioIndexes {
+ index: string;
+ create?: boolean;
+ downsample?: boolean;
+ removeTSDBFields?: boolean;
+ removeLogsDBFields?: boolean;
+ mode?: 'tsdb' | 'logsdb';
+}
+type GetScenarioFn = (initialIndex: string) => Array<{
+ name: string;
+ indexes: ScenarioIndexes[];
+}>;
+
+export function setupScenarioRunner(
+ getService: FtrProviderContext['getService'],
+ getPageObjects: FtrProviderContext['getPageObjects'],
+ getScenario: GetScenarioFn
+) {
+ const now = moment().utc();
+ const fromMoment = now.clone().subtract(1, 'hour');
+ const toMoment = now.clone();
+ const fromTimeForScenarios = fromMoment.format(TIME_PICKER_FORMAT);
+ const toTimeForScenarios = toMoment.format(TIME_PICKER_FORMAT);
+
+ function runTestsForEachScenario(
+ initialIndex: string,
+ scenarioMode: 'tsdb' | 'logsdb',
+ testingFn: (indexes: ScenarioIndexes[]) => void
+ ): void {
+ const { common, lens } = getPageObjects(['common', 'lens', 'dashboard']);
+ const es = getService('es');
+ const log = getService('log');
+ const dataStreams = getService('dataStreams');
+ const elasticChart = getService('elasticChart');
+ const indexPatterns = getService('indexPatterns');
+ const createDocs = getDocsGenerator(log, es, scenarioMode);
+
+ for (const { name, indexes } of getScenario(initialIndex)) {
+ describe(name, () => {
+ let dataViewName: string;
+ let downsampledTargetIndex: string = '';
+
+ before(async () => {
+ for (const {
+ index,
+ create,
+ downsample,
+ mode,
+ removeTSDBFields,
+ removeLogsDBFields,
+ } of indexes) {
+ // Validate the scenario config
+ if (downsample && mode !== 'tsdb') {
+ expect().fail('Cannot create a scenario with downsampled stream without tsdb');
+ }
+ // Kick off the creation
+ const isStream = mode !== undefined;
+ if (create) {
+ if (isStream) {
+ await dataStreams.createDataStream(
+ index,
+ getDataMapping({
+ mode,
+ removeTSDBFields: Boolean(removeTSDBFields || mode === 'logsdb'),
+ removeLogsDBFields,
+ }),
+ mode
+ );
+ } else {
+ log.info(`creating a index "${index}" with mapping...`);
+ await es.indices.create({
+ index,
+ mappings: {
+ properties: getDataMapping({
+ mode: mode === 'logsdb' ? 'logsdb' : 'tsdb', // use tsdb by default in regular index is specified
+ removeTSDBFields,
+ removeLogsDBFields,
+ }),
+ },
+ });
+ }
+ // add data to the newly created index
+ await createDocs(
+ index,
+ { isStream, removeTSDBFields, removeLogsDBFields },
+ fromTimeForScenarios
+ );
+ }
+ if (downsample) {
+ downsampledTargetIndex = await dataStreams.downsampleTSDBIndex(index, {
+ isStream: mode === 'tsdb',
+ });
+ }
+ }
+ dataViewName = `${indexes.map(({ index }) => index).join(',')}${
+ downsampledTargetIndex ? `,${downsampledTargetIndex}` : ''
+ }`;
+ log.info(`creating a data view for "${dataViewName}"...`);
+ await indexPatterns.create(
+ {
+ title: dataViewName,
+ timeFieldName: '@timestamp',
+ },
+ { override: true }
+ );
+ await common.navigateToApp('lens');
+ await elasticChart.setNewChartUiDebugFlag(true);
+ // go to the
+ await lens.goToTimeRange(
+ fromTimeForScenarios,
+ moment
+ .utc(toTimeForScenarios, TIME_PICKER_FORMAT)
+ .add(2, 'hour')
+ .format(TIME_PICKER_FORMAT) // consider also new documents
+ );
+ });
+
+ after(async () => {
+ for (const { index, create, mode: indexMode } of indexes) {
+ if (create) {
+ if (indexMode === 'tsdb' || indexMode === 'logsdb') {
+ await dataStreams.deleteDataStream(index);
+ } else {
+ log.info(`deleting the index "${index}"...`);
+ await es.indices.delete({
+ index,
+ });
+ }
+ }
+ // no need to cleant he specific downsample index as everything linked to the stream
+ // is cleaned up automatically
+ }
+ });
+
+ beforeEach(async () => {
+ await lens.switchDataPanelIndexPattern(dataViewName);
+ await lens.removeLayer();
+ });
+
+ testingFn(indexes);
+ });
+ }
+ }
+
+ return { runTestsForEachScenario, fromTimeForScenarios, toTimeForScenarios };
+}
diff --git a/x-pack/test/functional/services/data_stream.ts b/x-pack/test/functional/services/data_stream.ts
index 2864be1e0dc2b..f4b33213e62dd 100644
--- a/x-pack/test/functional/services/data_stream.ts
+++ b/x-pack/test/functional/services/data_stream.ts
@@ -9,7 +9,7 @@ import type { MappingProperty } from '@elastic/elasticsearch/lib/api/types';
import type { FtrProviderContext } from '../ftr_provider_context';
/**
- * High level interface to operate with Elasticsearch data stream and TSDS.
+ * High level interface to operate with Elasticsearch data stream and TSDS/LogsDB.
*/
export function DataStreamProvider({ getService, getPageObject }: FtrProviderContext) {
const es = getService('es');
@@ -112,23 +112,45 @@ export function DataStreamProvider({ getService, getPageObject }: FtrProviderCon
async function updateDataStreamTemplate(
stream: string,
mapping: Record,
- tsdb?: boolean
+ mode?: 'tsdb' | 'logsdb'
) {
await es.cluster.putComponentTemplate({
name: `${stream}_mapping`,
template: {
- settings: tsdb
- ? {
+ settings: !mode
+ ? { mode: undefined }
+ : mode === 'logsdb'
+ ? { mode: 'logsdb' }
+ : {
mode: 'time_series',
routing_path: 'request',
- }
- : { mode: undefined },
+ },
mappings: {
properties: mapping,
},
},
});
- log.info(`Updating ${stream} index template${tsdb ? ' for TSDB' : ''}...`);
+ // Uncomment only when needed
+ // log.debug(`
+ // PUT _component_template/${stream}_mappings
+ // ${JSON.stringify({
+ // name: `${stream}_mapping`,
+ // template: {
+ // settings: !mode
+ // ? { mode: undefined }
+ // : mode === 'logsdb'
+ // ? { mode: 'logsdb' }
+ // : {
+ // mode: 'time_series',
+ // routing_path: 'request',
+ // },
+ // mappings: {
+ // properties: mapping,
+ // },
+ // },
+ // }, null, 2)}
+ // `);
+ log.info(`Updating ${stream} index template${mode ? ` for ${mode.toUpperCase()}` : ''}...`);
await es.indices.putIndexTemplate({
name: `${stream}_index_template`,
index_patterns: [stream],
@@ -138,71 +160,98 @@ export function DataStreamProvider({ getService, getPageObject }: FtrProviderCon
description: `Template for ${stream} testing index`,
},
});
+ // Uncomment only when needed
+ // log.verbose(`
+ // PUT _index_template/${stream}-index-template
+ // ${JSON.stringify({
+ // name: `${stream}_index_template`,
+ // index_patterns: [stream],
+ // data_stream: {},
+ // composed_of: [`${stream}_mapping`],
+ // _meta: {
+ // description: `Template for ${stream} testing index`,
+ // },
+ // }, null, 2)}
+ // `);
}
/**
- * "Upgrade" a given data stream into a time series data series (TSDB/TSDS)
+ * "Upgrade" a given data stream into a TSDB or LogsDB data series
* @param stream the data stream name
* @param newMapping the new mapping already with time series metrics/dimensions configured
*/
- async function upgradeStreamToTSDB(stream: string, newMapping: Record) {
- // rollover to upgrade the index type to time_series
+ async function upgradeStream(
+ stream: string,
+ newMapping: Record,
+ mode: 'tsdb' | 'logsdb'
+ ) {
+ // rollover to upgrade the index type
// uploading a new mapping for the stream index using the provided metric/dimension list
- log.info(`Updating ${stream} data stream component template with TSDB stuff...`);
- await updateDataStreamTemplate(stream, newMapping, true);
+ log.info(`Updating ${stream} data stream component template with ${mode} stuff...`);
+ await updateDataStreamTemplate(stream, newMapping, mode);
- log.info('Rolling over the backing index for TSDB');
+ log.info(`Rolling over the backing index for ${mode}`);
await es.indices.rollover({
alias: stream,
});
+ // Uncomment only when needed
+ // log.verbose(`POST ${stream}/_rollover`);
}
/**
- * "Downgrade" a TSDB/TSDS data stream into a regular data stream
- * @param tsdbStream the TSDB/TSDS data stream to "downgrade"
+ * "Downgrade" a TSDB/TSDS/LogsDB data stream into a regular data stream
+ * @param stream the TSDB/TSDS/LogsDB data stream to "downgrade"
* @param oldMapping the new mapping already with time series metrics/dimensions already removed
*/
- async function downgradeTSDBtoStream(
- tsdbStream: string,
- newMapping: Record
+ async function downgradeStream(
+ stream: string,
+ newMapping: Record,
+ mode: 'tsdb' | 'logsdb'
) {
- // strip out any time-series specific mapping
- for (const fieldMapping of Object.values(newMapping || {})) {
- if ('time_series_metric' in fieldMapping) {
- delete fieldMapping.time_series_metric;
- }
- if ('time_series_dimension' in fieldMapping) {
- delete fieldMapping.time_series_dimension;
+ if (mode === 'tsdb') {
+ // strip out any time-series specific mapping
+ for (const fieldMapping of Object.values(newMapping || {})) {
+ if ('time_series_metric' in fieldMapping) {
+ delete fieldMapping.time_series_metric;
+ }
+ if ('time_series_dimension' in fieldMapping) {
+ delete fieldMapping.time_series_dimension;
+ }
}
+ log.info(`Updating ${stream} data stream component template with TSDB stuff...`);
+ await updateDataStreamTemplate(stream, newMapping);
}
- log.info(`Updating ${tsdbStream} data stream component template with TSDB stuff...`);
- await updateDataStreamTemplate(tsdbStream, newMapping, false);
+
// rollover to downgrade the index type to regular stream
- log.info(`Rolling over the ${tsdbStream} data stream into a regular data stream...`);
+ log.info(`Rolling over the ${stream} data stream into a regular data stream...`);
await es.indices.rollover({
- alias: tsdbStream,
+ alias: stream,
});
+ // Uncomment only when needed
+ // log.debug(`POST ${stream}/_rollover`);
}
/**
* Takes care of the entire process to create a data stream
* @param streamIndex name of the new data stream to create
* @param mappings the mapping to associate with the data stream
- * @param tsdb when enabled it will configure the data stream as a TSDB/TSDS
+ * @param tsdb when enabled it will configure the data stream as a TSDB/TSDS/LogsDB
*/
async function createDataStream(
streamIndex: string,
mappings: Record,
- tsdb: boolean = true
+ mode: 'tsdb' | 'logsdb' | undefined
) {
log.info(`Creating ${streamIndex} data stream component template...`);
- await updateDataStreamTemplate(streamIndex, mappings, tsdb);
+ await updateDataStreamTemplate(streamIndex, mappings, mode);
log.info(`Creating ${streamIndex} data stream index...`);
await es.indices.createDataStream({
name: streamIndex,
});
+ // Uncomment only when needed
+ // log.debug(`PUT _data_stream/${streamIndex}`);
}
/**
@@ -212,21 +261,27 @@ export function DataStreamProvider({ getService, getPageObject }: FtrProviderCon
async function deleteDataStream(streamIndex: string) {
log.info(`Delete ${streamIndex} data stream index...`);
await es.indices.deleteDataStream({ name: streamIndex });
+ // Uncomment only when needed
+ // log.debug(`DELETE _data_stream/${streamIndex}`);
log.info(`Delete ${streamIndex} index template...`);
await es.indices.deleteIndexTemplate({
name: `${streamIndex}_index_template`,
});
+ // Uncomment only when needed
+ // log.debug(`DELETE _index_template/${streamIndex}-index-template`);
log.info(`Delete ${streamIndex} data stream component template...`);
await es.cluster.deleteComponentTemplate({
name: `${streamIndex}_mapping`,
});
+ // Uncomment only when needed
+ // log.debug(`DELETE _component_template/${streamIndex}_mappings`);
}
return {
createDataStream,
deleteDataStream,
downsampleTSDBIndex,
- upgradeStreamToTSDB,
- downgradeTSDBtoStream,
+ upgradeStream,
+ downgradeStream,
};
}
diff --git a/x-pack/test/security_solution_cypress/cypress/screens/all_cases.ts b/x-pack/test/security_solution_cypress/cypress/screens/all_cases.ts
index 8dbc850fb017c..dbc9cd7c83f0e 100644
--- a/x-pack/test/security_solution_cypress/cypress/screens/all_cases.ts
+++ b/x-pack/test/security_solution_cypress/cypress/screens/all_cases.ts
@@ -5,8 +5,7 @@
* 2.0.
*/
-export const ALL_CASES_CLOSED_CASES_STATS =
- '[data-test-subj="closedStatsHeader"] .euiDescriptionList__description';
+export const ALL_CASES_CLOSED_CASES_STATS = '[data-test-subj="closedStatsHeader"] .euiStat__title';
export const ALL_CASES_COMMENTS_COUNT = '[data-test-subj="case-table-column-commentCount"]';
@@ -15,7 +14,7 @@ export const ALL_CASES_CREATE_NEW_CASE_BTN = '[data-test-subj="createNewCaseBtn"
export const ALL_CASES_CREATE_NEW_CASE_TABLE_BTN = '[data-test-subj="cases-table-add-case"]';
export const ALL_CASES_IN_PROGRESS_CASES_STATS =
- '[data-test-subj="inProgressStatsHeader"] .euiDescriptionList__description';
+ '[data-test-subj="inProgressStatsHeader"] .euiStat__title';
export const ALL_CASES_NAME = '[data-test-subj="case-details-link"]';
@@ -27,8 +26,7 @@ export const ALL_CASES_STATUS_FILTER = '[data-test-subj="options-filter-popover-
export const ALL_CASES_OPEN_FILTER = '[data-test-subj="options-filter-popover-item-open"]';
-export const ALL_CASES_OPEN_CASES_STATS =
- '[data-test-subj="openStatsHeader"] .euiDescriptionList__description';
+export const ALL_CASES_OPEN_CASES_STATS = '[data-test-subj="openStatsHeader"] .euiStat__title';
export const ALL_CASES_OPENED_ON = '[data-test-subj="case-table-column-createdAt"]';
diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json
index 8918b2848bc36..038e9baa5086a 100644
--- a/x-pack/test/tsconfig.json
+++ b/x-pack/test/tsconfig.json
@@ -168,6 +168,8 @@
"@kbn/reporting-server",
"@kbn/data-quality-plugin",
"@kbn/ml-trained-models-utils",
+ "@kbn/observability-synthetics-test-data",
+ "@kbn/ml-trained-models-utils",
"@kbn/openapi-common",
"@kbn/securitysolution-lists-common",
"@kbn/securitysolution-exceptions-common",
@@ -181,6 +183,7 @@
"@kbn/management-settings-ids",
"@kbn/mock-idp-utils",
"@kbn/cloud-security-posture-common",
- "@kbn/saved-objects-management-plugin"
+ "@kbn/saved-objects-management-plugin",
+ "@kbn/alerting-types"
]
}
diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts
index 01c2ebf9a64a7..b3db98c829afd 100644
--- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts
@@ -11,9 +11,9 @@ import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '@kbn/cloud-security-posture-pl
import * as http from 'http';
import {
deleteIndex,
- addIndex,
createPackagePolicy,
createCloudDefendPackagePolicy,
+ bulkIndex,
} from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper';
import { RoleCredentials } from '../../../../../shared/services';
import { getMockFindings, getMockDefendForContainersHeartbeats } from './mock_data';
@@ -38,8 +38,7 @@ export default function (providerContext: FtrProviderContext) {
The task manager is running by default in security serverless project in the background and sending usage API requests to the usage API.
This test mocks the usage API server and intercepts the usage API request sent by the metering background task manager.
*/
- // FLAKY: https://github.com/elastic/kibana/issues/188660
- describe.skip('Intercept the usage API request sent by the metering background task manager', function () {
+ describe('Intercept the usage API request sent by the metering background task manager', function () {
this.tags(['skipMKI']);
let mockUsageApiServer: http.Server;
@@ -117,7 +116,7 @@ export default function (providerContext: FtrProviderContext) {
numberOfFindings: 10,
});
- await addIndex(
+ await bulkIndex(
es,
[...billableFindings, ...notBillableFindings],
LATEST_FINDINGS_INDEX_DEFAULT_NS
@@ -161,7 +160,7 @@ export default function (providerContext: FtrProviderContext) {
numberOfFindings: 11,
});
- await addIndex(
+ await bulkIndex(
es,
[...billableFindings, ...notBillableFindings],
LATEST_FINDINGS_INDEX_DEFAULT_NS
@@ -200,7 +199,7 @@ export default function (providerContext: FtrProviderContext) {
numberOfFindings: 2,
});
- await addIndex(es, billableFindings, CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN);
+ await bulkIndex(es, billableFindings, CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN);
let interceptedRequestBody: UsageRecord[] = [];
@@ -234,7 +233,7 @@ export default function (providerContext: FtrProviderContext) {
isBlockActionEnables: false,
numberOfHearbeats: 2,
});
- await addIndex(
+ await bulkIndex(
es,
[...blockActionEnabledHeartbeats, ...blockActionDisabledHeartbeats],
CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS
@@ -316,7 +315,7 @@ export default function (providerContext: FtrProviderContext) {
});
await Promise.all([
- addIndex(
+ bulkIndex(
es,
[
...billableFindingsCSPM,
@@ -326,8 +325,8 @@ export default function (providerContext: FtrProviderContext) {
],
LATEST_FINDINGS_INDEX_DEFAULT_NS
),
- addIndex(es, [...billableFindingsCNVM], CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN),
- addIndex(
+ bulkIndex(es, [...billableFindingsCNVM], CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN),
+ bulkIndex(
es,
[...blockActionEnabledHeartbeats, ...blockActionDisabledHeartbeats],
CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS
diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts
index 2455c8e1762cc..5e5844eaaf3b5 100644
--- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts
@@ -95,13 +95,13 @@ export const getMockDefendForContainersHeartbeats = ({
mockDefendForContainersHeartbeats(isBlockActionEnables)
);
};
-const mockDefendForContainersHeartbeats = (isBlockActionEnables: boolean) => {
+const mockDefendForContainersHeartbeats = (isBlockActionEnabled: boolean) => {
return {
agent: {
id: chance.guid(),
},
cloud_defend: {
- block_action_enabled: isBlockActionEnables,
+ block_action_enabled: isBlockActionEnabled,
},
event: {
ingested: new Date().toISOString(),
diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts
index 1f834cce9d847..615e3397a45ce 100644
--- a/x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts
+++ b/x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts
@@ -50,7 +50,7 @@ export function SvlSearchConnectorsPageProvider({ getService }: FtrProviderConte
expect(await testSubjects.getVisibleText('serverlessSearchConnectorName')).to.be(name);
},
async editType(type: string) {
- await testSubjects.existOrFail('serverlessSearchEditConnectorTypeLabel');
+ await testSubjects.existOrFail('serverlessSearchEditConnectorType');
await testSubjects.existOrFail('serverlessSearchEditConnectorTypeChoices');
await testSubjects.click('serverlessSearchEditConnectorTypeChoices');
await testSubjects.exists('serverlessSearchConnectorServiceType-zoom');
diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/index.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/index.ts
index b6eeef5a70f90..db58761270144 100644
--- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/index.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/index.ts
@@ -75,6 +75,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext
loadTestFile(require.resolve('./smokescreen.ts'));
loadTestFile(require.resolve('./tsdb.ts'));
+ loadTestFile(require.resolve('./logsdb.ts'));
loadTestFile(require.resolve('./vega_chart.ts'));
});
};
diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/logsdb.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/logsdb.ts
new file mode 100644
index 0000000000000..4fe3046aa5dbe
--- /dev/null
+++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/logsdb.ts
@@ -0,0 +1,586 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import moment from 'moment';
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+import {
+ type ScenarioIndexes,
+ getDataMapping,
+ getDocsGenerator,
+ setupScenarioRunner,
+ TIME_PICKER_FORMAT,
+} from './tsdb_logsdb_helpers';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const { common, lens, discover, header } = getPageObjects([
+ 'common',
+ 'lens',
+ 'discover',
+ 'header',
+ ]);
+ const testSubjects = getService('testSubjects');
+ const find = getService('find');
+ const kibanaServer = getService('kibanaServer');
+ const es = getService('es');
+ const log = getService('log');
+ const dataStreams = getService('dataStreams');
+ const indexPatterns = getService('indexPatterns');
+ const esArchiver = getService('esArchiver');
+ const monacoEditor = getService('monacoEditor');
+ const retry = getService('retry');
+
+ const createDocs = getDocsGenerator(log, es, 'logsdb');
+
+ describe('lens logsdb', function () {
+ const logsdbIndex = 'kibana_sample_data_logslogsdb';
+ const logsdbDataView = logsdbIndex;
+ const logsdbEsArchive = 'test/functional/fixtures/es_archiver/kibana_sample_data_logs_logsdb';
+ const fromTime = 'Apr 16, 2023 @ 00:00:00.000';
+ const toTime = 'Jun 16, 2023 @ 00:00:00.000';
+
+ before(async () => {
+ log.info(`loading ${logsdbIndex} index...`);
+ await esArchiver.loadIfNeeded(logsdbEsArchive);
+ log.info(`creating a data view for "${logsdbDataView}"...`);
+ await indexPatterns.create(
+ {
+ title: logsdbDataView,
+ timeFieldName: '@timestamp',
+ },
+ { override: true }
+ );
+ log.info(`updating settings to use the "${logsdbDataView}" dataView...`);
+ await kibanaServer.uiSettings.update({
+ 'dateFormat:tz': 'UTC',
+ defaultIndex: '0ae0bc7a-e4ca-405c-ab67-f2b5913f2a51',
+ 'timepicker:timeDefaults': `{ "from": "${fromTime}", "to": "${toTime}" }`,
+ });
+ });
+
+ after(async () => {
+ await kibanaServer.savedObjects.cleanStandardList();
+ await kibanaServer.uiSettings.replace({});
+ await es.indices.delete({ index: [logsdbIndex] });
+ });
+
+ describe('smoke testing functions support', () => {
+ before(async () => {
+ await common.navigateToApp('lens');
+ await lens.switchDataPanelIndexPattern(logsdbDataView);
+ await lens.goToTimeRange();
+ });
+
+ afterEach(async () => {
+ await lens.removeLayer();
+ });
+
+ // skip count for now as it's a special function and will
+ // change automatically the unsupported field to Records when detected
+ const allOperations = [
+ 'average',
+ 'max',
+ 'last_value',
+ 'median',
+ 'percentile',
+ 'percentile_rank',
+ 'standard_deviation',
+ 'sum',
+ 'unique_count',
+ 'min',
+ 'max',
+ 'counter_rate',
+ 'last_value',
+ ];
+
+ it(`should work with all operations`, async () => {
+ // start from a count() over a date histogram
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: '@timestamp',
+ });
+
+ // minimum supports all logsdb field types
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'count',
+ field: 'bytes',
+ keepOpen: true,
+ });
+
+ // now check that operations won't show the incompatibility tooltip
+ for (const operation of allOperations) {
+ expect(
+ testSubjects.exists(`lns-indexPatternDimension-${operation} incompatible`, {
+ timeout: 500,
+ })
+ ).to.eql(false);
+ }
+
+ for (const operation of allOperations) {
+ // try to change to the provided function and check all is ok
+ await lens.selectOperation(operation);
+
+ expect(
+ await find.existsByCssSelector(
+ '[data-test-subj="indexPattern-field-selection-row"] .euiFormErrorText'
+ )
+ ).to.be(false);
+ }
+ await lens.closeDimensionEditor();
+ });
+
+ describe('Scenarios with changing stream type', () => {
+ const getScenarios = (
+ initialIndex: string
+ ): Array<{
+ name: string;
+ indexes: ScenarioIndexes[];
+ }> => [
+ {
+ name: 'LogsDB stream with no additional stream/index',
+ indexes: [{ index: initialIndex }],
+ },
+ {
+ name: 'LogsDB stream with no additional stream/index and no host.name field',
+ indexes: [
+ {
+ index: `${initialIndex}_no_host`,
+ removeLogsDBFields: true,
+ create: true,
+ mode: 'logsdb',
+ },
+ ],
+ },
+ {
+ name: 'LogsDB stream with an additional regular index',
+ indexes: [{ index: initialIndex }, { index: 'regular_index', create: true }],
+ },
+ {
+ name: 'LogsDB stream with an additional LogsDB stream',
+ indexes: [
+ { index: initialIndex },
+ { index: 'logsdb_index_2', create: true, mode: 'logsdb' },
+ ],
+ },
+ {
+ name: 'LogsDB stream with an additional TSDB stream',
+ indexes: [{ index: initialIndex }, { index: 'tsdb_index', create: true, mode: 'tsdb' }],
+ },
+ {
+ name: 'LogsDB stream with an additional TSDB stream downsampled',
+ indexes: [
+ { index: initialIndex },
+ { index: 'tsdb_index_downsampled', create: true, mode: 'tsdb', downsample: true },
+ ],
+ },
+ ];
+
+ const { runTestsForEachScenario, toTimeForScenarios, fromTimeForScenarios } =
+ setupScenarioRunner(getService, getPageObjects, getScenarios);
+
+ describe('Data-stream upgraded to LogsDB scenarios', () => {
+ const streamIndex = 'data_stream';
+ // rollover does not allow to change name, it will just change backing index underneath
+ const streamConvertedToLogsDBIndex = streamIndex;
+
+ before(async () => {
+ log.info(`Creating "${streamIndex}" data stream...`);
+ await dataStreams.createDataStream(
+ streamIndex,
+ getDataMapping({ mode: 'logsdb' }),
+ undefined
+ );
+
+ // add some data to the stream
+ await createDocs(streamIndex, { isStream: true }, fromTimeForScenarios);
+
+ log.info(`Update settings for "${streamIndex}" dataView...`);
+ await kibanaServer.uiSettings.update({
+ 'dateFormat:tz': 'UTC',
+ 'timepicker:timeDefaults': '{ "from": "now-1y", "to": "now" }',
+ });
+ log.info(`Upgrade "${streamIndex}" stream to LogsDB...`);
+
+ const logsdbMapping = getDataMapping({ mode: 'logsdb' });
+ await dataStreams.upgradeStream(streamIndex, logsdbMapping, 'logsdb');
+ log.info(
+ `Add more data to new "${streamConvertedToLogsDBIndex}" dataView (now with LogsDB backing index)...`
+ );
+ // add some more data when upgraded
+ await createDocs(streamConvertedToLogsDBIndex, { isStream: true }, toTimeForScenarios);
+ });
+
+ after(async () => {
+ await dataStreams.deleteDataStream(streamIndex);
+ });
+
+ runTestsForEachScenario(streamConvertedToLogsDBIndex, 'logsdb', (indexes) => {
+ it(`should visualize a date histogram chart`, async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: '@timestamp',
+ });
+
+ // check that a basic agg on a field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+
+ await lens.waitForVisualization('xyVisChart');
+ const data = await lens.getCurrentChartDebugState('xyVisChart');
+ const bars = data?.bars![0].bars;
+
+ log.info('Check counter data before the upgrade');
+ // check there's some data before the upgrade
+ expect(bars?.[0].y).to.be.above(0);
+ log.info('Check counter data after the upgrade');
+ // check there's some data after the upgrade
+ expect(bars?.[bars.length - 1].y).to.be.above(0);
+ });
+
+ it(`should visualize a date histogram chart using a different date field`, async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+
+ await lens.waitForVisualization('xyVisChart');
+ const data = await lens.getCurrentChartDebugState('xyVisChart');
+ const bars = data?.bars![0].bars;
+
+ log.info('Check counter data before the upgrade');
+ // check there's some data before the upgrade
+ expect(bars?.[0].y).to.be.above(0);
+ log.info('Check counter data after the upgrade');
+ // check there's some data after the upgrade
+ expect(bars?.[bars.length - 1].y).to.be.above(0);
+ });
+
+ it('should visualize an annotation layer from a logsDB stream', async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+ await lens.createLayer('annotations');
+
+ expect(
+ (await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length
+ ).to.eql(2);
+ expect(
+ await (
+ await testSubjects.find('lnsXY_xAnnotationsPanel > lns-dimensionTrigger')
+ ).getVisibleText()
+ ).to.eql('Event');
+ await testSubjects.click('lnsXY_xAnnotationsPanel > lns-dimensionTrigger');
+ await testSubjects.click('lnsXY_annotation_query');
+ await lens.configureQueryAnnotation({
+ queryString: 'host.name: *',
+ timeField: '@timestamp',
+ textDecoration: { type: 'name' },
+ extraFields: ['host.name', 'utc_time'],
+ });
+ await lens.closeDimensionEditor();
+
+ await testSubjects.existOrFail('xyVisGroupedAnnotationIcon');
+ await lens.removeLayer(1);
+ });
+
+ it('should visualize an annotation layer from a logsDB stream using another time field', async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+ await lens.createLayer('annotations');
+
+ expect(
+ (await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length
+ ).to.eql(2);
+ expect(
+ await (
+ await testSubjects.find('lnsXY_xAnnotationsPanel > lns-dimensionTrigger')
+ ).getVisibleText()
+ ).to.eql('Event');
+ await testSubjects.click('lnsXY_xAnnotationsPanel > lns-dimensionTrigger');
+ await testSubjects.click('lnsXY_annotation_query');
+ await lens.configureQueryAnnotation({
+ queryString: 'host.name: *',
+ timeField: 'utc_time',
+ textDecoration: { type: 'name' },
+ extraFields: ['host.name', '@timestamp'],
+ });
+ await lens.closeDimensionEditor();
+
+ await testSubjects.existOrFail('xyVisGroupedAnnotationIcon');
+ await lens.removeLayer(1);
+ });
+
+ it('should visualize correctly ES|QL queries based on a LogsDB stream', async () => {
+ await common.navigateToApp('discover');
+ await discover.selectTextBaseLang();
+ await header.waitUntilLoadingHasFinished();
+ await monacoEditor.setCodeEditorValue(
+ `from ${indexes
+ .map(({ index }) => index)
+ .join(', ')} | stats averageB = avg(bytes) by extension`
+ );
+ await testSubjects.click('querySubmitButton');
+ await header.waitUntilLoadingHasFinished();
+ await testSubjects.click('unifiedHistogramEditFlyoutVisualization');
+
+ await header.waitUntilLoadingHasFinished();
+
+ await retry.waitFor('lens flyout', async () => {
+ const dimensions = await testSubjects.findAll('lns-dimensionTrigger-textBased');
+ return (
+ dimensions.length === 2 && (await dimensions[1].getVisibleText()) === 'averageB'
+ );
+ });
+
+ // go back to Lens to not break the wrapping function
+ await common.navigateToApp('lens');
+ });
+ });
+ });
+
+ describe('LogsDB downgraded to regular data stream scenarios', () => {
+ const logsdbStream = 'logsdb_stream_dowgradable';
+ // rollover does not allow to change name, it will just change backing index underneath
+ const logsdbConvertedToStream = logsdbStream;
+
+ before(async () => {
+ log.info(`Creating "${logsdbStream}" data stream...`);
+ await dataStreams.createDataStream(
+ logsdbStream,
+ getDataMapping({ mode: 'logsdb' }),
+ 'logsdb'
+ );
+
+ // add some data to the stream
+ await createDocs(logsdbStream, { isStream: true }, fromTimeForScenarios);
+
+ log.info(`Update settings for "${logsdbStream}" dataView...`);
+ await kibanaServer.uiSettings.update({
+ 'dateFormat:tz': 'UTC',
+ 'timepicker:timeDefaults': '{ "from": "now-1y", "to": "now" }',
+ });
+ log.info(
+ `Dowgrade "${logsdbStream}" stream into regular stream "${logsdbConvertedToStream}"...`
+ );
+
+ await dataStreams.downgradeStream(
+ logsdbStream,
+ getDataMapping({ mode: 'logsdb' }),
+ 'logsdb'
+ );
+ log.info(
+ `Add more data to new "${logsdbConvertedToStream}" dataView (no longer LogsDB)...`
+ );
+ // add some more data when upgraded
+ await createDocs(logsdbConvertedToStream, { isStream: true }, toTimeForScenarios);
+ });
+
+ after(async () => {
+ await dataStreams.deleteDataStream(logsdbConvertedToStream);
+ });
+
+ runTestsForEachScenario(logsdbConvertedToStream, 'logsdb', (indexes) => {
+ it(`should visualize a date histogram chart`, async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: '@timestamp',
+ });
+
+ // check that a basic agg on a field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+
+ await lens.waitForVisualization('xyVisChart');
+ const data = await lens.getCurrentChartDebugState('xyVisChart');
+ const bars = data?.bars![0].bars;
+
+ log.info('Check counter data before the upgrade');
+ // check there's some data before the upgrade
+ expect(bars?.[0].y).to.be.above(0);
+ log.info('Check counter data after the upgrade');
+ // check there's some data after the upgrade
+ expect(bars?.[bars.length - 1].y).to.be.above(0);
+ });
+
+ it(`should visualize a date histogram chart using a different date field`, async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+
+ await lens.waitForVisualization('xyVisChart');
+ const data = await lens.getCurrentChartDebugState('xyVisChart');
+ const bars = data?.bars![0].bars;
+
+ log.info('Check counter data before the upgrade');
+ // check there's some data before the upgrade
+ expect(bars?.[0].y).to.be.above(0);
+ log.info('Check counter data after the upgrade');
+ // check there's some data after the upgrade
+ expect(bars?.[bars.length - 1].y).to.be.above(0);
+ });
+
+ it('should visualize an annotation layer from a logsDB stream', async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+ await lens.createLayer('annotations');
+
+ expect(
+ (await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length
+ ).to.eql(2);
+ expect(
+ await (
+ await testSubjects.find('lnsXY_xAnnotationsPanel > lns-dimensionTrigger')
+ ).getVisibleText()
+ ).to.eql('Event');
+ await testSubjects.click('lnsXY_xAnnotationsPanel > lns-dimensionTrigger');
+ await testSubjects.click('lnsXY_annotation_query');
+ await lens.configureQueryAnnotation({
+ queryString: 'host.name: *',
+ timeField: '@timestamp',
+ textDecoration: { type: 'name' },
+ extraFields: ['host.name', 'utc_time'],
+ });
+ await lens.closeDimensionEditor();
+
+ await testSubjects.existOrFail('xyVisGroupedAnnotationIcon');
+ await lens.removeLayer(1);
+ });
+
+ it('should visualize an annotation layer from a logsDB stream using another time field', async () => {
+ await lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: 'utc_time',
+ });
+
+ // check the counter field works
+ await lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'min',
+ field: `bytes`,
+ });
+ await lens.createLayer('annotations');
+
+ expect(
+ (await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length
+ ).to.eql(2);
+ expect(
+ await (
+ await testSubjects.find('lnsXY_xAnnotationsPanel > lns-dimensionTrigger')
+ ).getVisibleText()
+ ).to.eql('Event');
+ await testSubjects.click('lnsXY_xAnnotationsPanel > lns-dimensionTrigger');
+ await testSubjects.click('lnsXY_annotation_query');
+ await lens.configureQueryAnnotation({
+ queryString: 'host.name: *',
+ timeField: 'utc_time',
+ textDecoration: { type: 'name' },
+ extraFields: ['host.name', '@timestamp'],
+ });
+ await lens.closeDimensionEditor();
+
+ await testSubjects.existOrFail('xyVisGroupedAnnotationIcon');
+ await lens.removeLayer(1);
+ });
+
+ it('should visualize correctly ES|QL queries based on a LogsDB stream', async () => {
+ await common.navigateToApp('discover');
+ await discover.selectTextBaseLang();
+
+ // Use the lens page object here also for discover: both use the same timePicker object
+ await lens.goToTimeRange(
+ fromTimeForScenarios,
+ moment
+ .utc(toTimeForScenarios, TIME_PICKER_FORMAT)
+ .add(2, 'hour')
+ .format(TIME_PICKER_FORMAT)
+ );
+
+ await header.waitUntilLoadingHasFinished();
+ await monacoEditor.setCodeEditorValue(
+ `from ${indexes
+ .map(({ index }) => index)
+ .join(', ')} | stats averageB = avg(bytes) by extension`
+ );
+ await testSubjects.click('querySubmitButton');
+ await header.waitUntilLoadingHasFinished();
+ await testSubjects.click('unifiedHistogramEditFlyoutVisualization');
+
+ await header.waitUntilLoadingHasFinished();
+
+ await retry.waitFor('lens flyout', async () => {
+ const dimensions = await testSubjects.findAll('lns-dimensionTrigger-textBased');
+ return (
+ dimensions.length === 2 && (await dimensions[1].getVisibleText()) === 'averageB'
+ );
+ });
+
+ // go back to Lens to not break the wrapping function
+ await common.navigateToApp('lens');
+ });
+ });
+ });
+ });
+ });
+ });
+}
diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/tsdb.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/tsdb.ts
index 99633e01940c1..111cf30e919c5 100644
--- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/tsdb.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/tsdb.ts
@@ -8,239 +8,20 @@
import expect from '@kbn/expect';
import { partition } from 'lodash';
import moment from 'moment';
-import { MappingProperty } from '@elastic/elasticsearch/lib/api/types';
import { FtrProviderContext } from '../../../../ftr_provider_context';
-
-const TEST_DOC_COUNT = 100;
-const TIME_PICKER_FORMAT = 'MMM D, YYYY [@] HH:mm:ss.SSS';
-const timeSeriesMetrics: Record = {
- bytes_gauge: 'gauge',
- bytes_counter: 'counter',
-};
-const timeSeriesDimensions = ['request', 'url'];
-
-type TestDoc = Record>;
-
-const testDocTemplate: TestDoc = {
- agent: 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1',
- bytes: 6219,
- clientip: '223.87.60.27',
- extension: 'deb',
- geo: {
- srcdest: 'US:US',
- src: 'US',
- dest: 'US',
- coordinates: { lat: 39.41042861, lon: -88.8454325 },
- },
- host: 'artifacts.elastic.co',
- index: 'kibana_sample_data_logs',
- ip: '223.87.60.27',
- machine: { ram: 8589934592, os: 'win 8' },
- memory: null,
- message:
- '223.87.60.27 - - [2018-07-22T00:39:02.912Z] "GET /elasticsearch/elasticsearch-6.3.2.deb_1 HTTP/1.1" 200 6219 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1"',
- phpmemory: null,
- referer: 'http://twitter.com/success/wendy-lawrence',
- request: '/elasticsearch/elasticsearch-6.3.2.deb',
- response: 200,
- tags: ['success', 'info'],
- '@timestamp': '2018-07-22T00:39:02.912Z',
- url: 'https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.3.2.deb_1',
- utc_time: '2018-07-22T00:39:02.912Z',
- event: { dataset: 'sample_web_logs' },
- bytes_gauge: 0,
- bytes_counter: 0,
-};
-
-function getDataMapping(
- { tsdb, removeTSDBFields }: { tsdb: boolean; removeTSDBFields?: boolean } = {
- tsdb: false,
- }
-): Record {
- const dataStreamMapping: Record = {
- '@timestamp': {
- type: 'date',
- },
- agent: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- bytes: {
- type: 'long',
- },
- bytes_counter: {
- type: 'long',
- },
- bytes_gauge: {
- type: 'long',
- },
- clientip: {
- type: 'ip',
- },
- event: {
- properties: {
- dataset: {
- type: 'keyword',
- },
- },
- },
- extension: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- geo: {
- properties: {
- coordinates: {
- type: 'geo_point',
- },
- dest: {
- type: 'keyword',
- },
- src: {
- type: 'keyword',
- },
- srcdest: {
- type: 'keyword',
- },
- },
- },
- host: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- index: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- ip: {
- type: 'ip',
- },
- machine: {
- properties: {
- os: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- ram: {
- type: 'long',
- },
- },
- },
- memory: {
- type: 'double',
- },
- message: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- phpmemory: {
- type: 'long',
- },
- referer: {
- type: 'keyword',
- },
- request: {
- type: 'keyword',
- },
- response: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- tags: {
- fields: {
- keyword: {
- ignore_above: 256,
- type: 'keyword',
- },
- },
- type: 'text',
- },
- timestamp: {
- path: '@timestamp',
- type: 'alias',
- },
- url: {
- type: 'keyword',
- },
- utc_time: {
- type: 'date',
- },
- };
-
- if (tsdb) {
- // augment the current mapping
- for (const [fieldName, fieldMapping] of Object.entries(dataStreamMapping || {})) {
- if (
- timeSeriesMetrics[fieldName] &&
- (fieldMapping.type === 'double' || fieldMapping.type === 'long')
- ) {
- fieldMapping.time_series_metric = timeSeriesMetrics[fieldName];
- }
-
- if (timeSeriesDimensions.includes(fieldName) && fieldMapping.type === 'keyword') {
- fieldMapping.time_series_dimension = true;
- }
- }
- } else if (removeTSDBFields) {
- for (const fieldName of Object.keys(timeSeriesMetrics)) {
- delete dataStreamMapping[fieldName];
- }
- }
- return dataStreamMapping;
-}
-
-function sumFirstNValues(n: number, bars: Array<{ y: number }>): number {
- const indexes = Array(n)
- .fill(1)
- .map((_, i) => i);
- let countSum = 0;
- for (const index of indexes) {
- if (bars[index]) {
- countSum += bars[index].y;
- }
- }
- return countSum;
-}
+import {
+ type ScenarioIndexes,
+ TEST_DOC_COUNT,
+ TIME_PICKER_FORMAT,
+ getDataMapping,
+ getDocsGenerator,
+ setupScenarioRunner,
+ sumFirstNValues,
+} from './tsdb_logsdb_helpers';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const PageObjects = getPageObjects([
+ const { common, lens, dashboard, svlCommonPage } = getPageObjects([
'common',
- 'timePicker',
'lens',
'dashboard',
'svlCommonPage',
@@ -251,71 +32,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const es = getService('es');
const log = getService('log');
const dataStreams = getService('dataStreams');
- const elasticChart = getService('elasticChart');
const indexPatterns = getService('indexPatterns');
const esArchiver = getService('esArchiver');
const comboBox = getService('comboBox');
- const createDocs = async (
- esIndex: string,
- { isStream, removeTSDBFields }: { isStream: boolean; removeTSDBFields?: boolean },
- startTime: string
- ) => {
- log.info(
- `Adding ${TEST_DOC_COUNT} to ${esIndex} with starting time from ${moment
- .utc(startTime, TIME_PICKER_FORMAT)
- .format(TIME_PICKER_FORMAT)} to ${moment
- .utc(startTime, TIME_PICKER_FORMAT)
- .add(2 * TEST_DOC_COUNT, 'seconds')
- .format(TIME_PICKER_FORMAT)}`
- );
- const docs = Array(TEST_DOC_COUNT)
- .fill(testDocTemplate)
- .map((templateDoc, i) => {
- const timestamp = moment
- .utc(startTime, TIME_PICKER_FORMAT)
- .add(TEST_DOC_COUNT + i, 'seconds')
- .format();
- const doc: TestDoc = {
- ...templateDoc,
- '@timestamp': timestamp,
- utc_time: timestamp,
- bytes_gauge: Math.floor(Math.random() * 10000 * i),
- bytes_counter: 5000,
- };
- if (removeTSDBFields) {
- for (const field of Object.keys(timeSeriesMetrics)) {
- delete doc[field];
- }
- }
- return doc;
- });
-
- const result = await es.bulk(
- {
- index: esIndex,
- body: docs.map((d) => `{"${isStream ? 'create' : 'index'}": {}}\n${JSON.stringify(d)}\n`),
- },
- { meta: true }
- );
-
- const res = result.body;
-
- if (res.errors) {
- const resultsWithErrors = res.items
- .filter(({ index }) => index?.error)
- .map(({ index }) => index?.error);
- for (const error of resultsWithErrors) {
- log.error(`Error: ${JSON.stringify(error)}`);
- }
- const [indexExists, dataStreamExists] = await Promise.all([
- es.indices.exists({ index: esIndex }),
- es.indices.getDataStream({ name: esIndex }),
- ]);
- log.debug(`Index exists: ${indexExists} - Data stream exists: ${dataStreamExists}`);
- }
- log.info(`Indexed ${res.items.length} test data docs.`);
- };
+ const createDocs = getDocsGenerator(log, es, 'tsdb');
describe('lens tsdb', function () {
const tsdbIndex = 'kibana_sample_data_logstsdb';
@@ -325,7 +46,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const toTime = 'Jun 16, 2023 @ 00:00:00.000';
before(async () => {
- await PageObjects.svlCommonPage.loginAsAdmin();
+ await svlCommonPage.loginAsAdmin();
log.info(`loading ${tsdbIndex} index...`);
await esArchiver.loadIfNeeded(tsdbEsArchive);
log.info(`creating a data view for "${tsdbDataView}"...`);
@@ -375,48 +96,48 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('for regular metric', () => {
it('defaults to median for non-rolled up metric', async () => {
- await PageObjects.common.navigateToApp('lens');
- await PageObjects.lens.switchDataPanelIndexPattern(tsdbDataView);
- await PageObjects.lens.waitForField('bytes_gauge');
- await PageObjects.lens.dragFieldToWorkspace('bytes_gauge', 'xyVisChart');
- expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql(
+ await common.navigateToApp('lens');
+ await lens.switchDataPanelIndexPattern(tsdbDataView);
+ await lens.waitForField('bytes_gauge');
+ await lens.dragFieldToWorkspace('bytes_gauge', 'xyVisChart');
+ expect(await lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql(
'Median of bytes_gauge'
);
});
it('does not show a warning', async () => {
- await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel');
+ await lens.openDimensionEditor('lnsXY_yDimensionPanel');
await testSubjects.missingOrFail('median-partial-warning');
- await PageObjects.lens.assertNoEditorWarning();
- await PageObjects.lens.closeDimensionEditor();
+ await lens.assertNoEditorWarning();
+ await lens.closeDimensionEditor();
});
});
describe('for rolled up metric (downsampled)', () => {
it('defaults to average for rolled up metric', async () => {
- await PageObjects.lens.switchDataPanelIndexPattern(downsampleDataView.dataView);
- await PageObjects.lens.removeLayer();
- await PageObjects.lens.waitForField('bytes_gauge');
- await PageObjects.lens.dragFieldToWorkspace('bytes_gauge', 'xyVisChart');
- expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql(
+ await lens.switchDataPanelIndexPattern(downsampleDataView.dataView);
+ await lens.removeLayer();
+ await lens.waitForField('bytes_gauge');
+ await lens.dragFieldToWorkspace('bytes_gauge', 'xyVisChart');
+ expect(await lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql(
'Average of bytes_gauge'
);
});
it('shows warnings in editor when using median', async () => {
- await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel');
+ await lens.openDimensionEditor('lnsXY_yDimensionPanel');
await testSubjects.existOrFail('median-partial-warning');
await testSubjects.click('lns-indexPatternDimension-median');
- await PageObjects.lens.waitForVisualization('xyVisChart');
- await PageObjects.lens.assertMessageListContains(
+ await lens.waitForVisualization('xyVisChart');
+ await lens.assertMessageListContains(
'Median of bytes_gauge uses a function that is unsupported by rolled up data. Select a different function or change the time range.',
'warning'
);
});
it('shows warnings in dashboards as well', async () => {
- await PageObjects.lens.save('New', false, false, false, 'new');
+ await lens.save('New', false, false, false, 'new');
- await PageObjects.dashboard.waitForRenderComplete();
- await PageObjects.lens.assertMessageListContains(
+ await dashboard.waitForRenderComplete();
+ await lens.assertMessageListContains(
'Median of bytes_gauge uses a function that is unsupported by rolled up data. Select a different function or change the time range.',
'warning'
);
@@ -426,13 +147,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('time series special field types support', () => {
before(async () => {
- await PageObjects.common.navigateToApp('lens');
- await PageObjects.lens.switchDataPanelIndexPattern(tsdbDataView);
- await PageObjects.lens.goToTimeRange();
+ await common.navigateToApp('lens');
+ await lens.switchDataPanelIndexPattern(tsdbDataView);
+ await lens.goToTimeRange();
});
afterEach(async () => {
- await PageObjects.lens.removeLayer();
+ await lens.removeLayer();
});
// skip count for now as it's a special function and will
@@ -467,14 +188,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
if (supportedOperations.length) {
it(`should allow operations when supported by ${fieldType} field type`, async () => {
// Counter rate requires a date histogram dimension configured to work
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
// minimum supports all tsdb field types
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'min',
field: `bytes_${fieldType}`,
@@ -492,7 +213,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
for (const supportedOp of supportedOperations) {
// try to change to the provided function and check all is ok
- await PageObjects.lens.selectOperation(supportedOp.name);
+ await lens.selectOperation(supportedOp.name);
expect(
await find.existsByCssSelector(
@@ -501,22 +222,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
).to.be(false);
// return in a clean state before checking the next operation
- await PageObjects.lens.selectOperation('min');
+ await lens.selectOperation('min');
}
- await PageObjects.lens.closeDimensionEditor();
+ await lens.closeDimensionEditor();
});
}
if (unsupportedOperatons.length) {
it(`should notify the incompatibility of unsupported operations for the ${fieldType} field type`, async () => {
// Counter rate requires a date histogram dimension configured to work
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
// minimum supports all tsdb field types
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'min',
field: `bytes_${fieldType}`,
@@ -537,7 +258,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
for (const unsupportedOp of unsupportedOperatons) {
// try to change to the provided function and check if it's in an incompatibility state
- await PageObjects.lens.selectOperation(unsupportedOp.name, true);
+ await lens.selectOperation(unsupportedOp.name, true);
const fieldSelectErrorEl = await find.byCssSelector(
'[data-test-subj="indexPattern-field-selection-row"] .euiFormErrorText'
@@ -548,28 +269,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
// return in a clean state before checking the next operation
- await PageObjects.lens.selectOperation('min');
+ await lens.selectOperation('min');
}
- await PageObjects.lens.closeDimensionEditor();
+ await lens.closeDimensionEditor();
});
}
}
describe('show time series dimension groups within breakdown', () => {
it('should show the time series dimension group on field picker when configuring a breakdown', async () => {
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'min',
field: 'bytes_counter',
});
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
operation: 'terms',
keepOpen: true,
@@ -577,46 +298,34 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const list = await comboBox.getOptionsList('indexPattern-dimension-field');
expect(list).to.contain('Time series dimensions');
- await PageObjects.lens.closeDimensionEditor();
+ await lens.closeDimensionEditor();
});
it("should not show the time series dimension group on field picker if it's not a breakdown", async () => {
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'min',
field: 'bytes_counter',
});
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
keepOpen: true,
});
const list = await comboBox.getOptionsList('indexPattern-dimension-field');
expect(list).to.not.contain('Time series dimensions');
- await PageObjects.lens.closeDimensionEditor();
+ await lens.closeDimensionEditor();
});
});
});
describe('Scenarios with changing stream type', () => {
- const now = moment().utc();
- const fromMoment = now.clone().subtract(1, 'hour');
- const toMoment = now.clone();
- const fromTimeForScenarios = fromMoment.format(TIME_PICKER_FORMAT);
- const toTimeForScenarios = toMoment.format(TIME_PICKER_FORMAT);
-
const getScenarios = (
initialIndex: string
): Array<{
name: string;
- indexes: Array<{
- index: string;
- create?: boolean;
- downsample?: boolean;
- tsdb?: boolean;
- removeTSDBFields?: boolean;
- }>;
+ indexes: ScenarioIndexes[];
}> => [
{
name: 'Dataview with no additional stream/index',
@@ -633,7 +342,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
name: 'Dataview with an additional downsampled TSDB stream',
indexes: [
{ index: initialIndex },
- { index: 'tsdb_index_2', create: true, tsdb: true, downsample: true },
+ { index: 'tsdb_index_2', create: true, mode: 'tsdb', downsample: true },
],
},
{
@@ -641,112 +350,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
indexes: [
{ index: initialIndex },
{ index: 'regular_index', create: true, removeTSDBFields: true },
- { index: 'tsdb_index_2', create: true, tsdb: true, downsample: true },
+ { index: 'tsdb_index_2', create: true, mode: 'tsdb', downsample: true },
],
},
{
name: 'Dataview with an additional TSDB stream',
- indexes: [{ index: initialIndex }, { index: 'tsdb_index_2', create: true, tsdb: true }],
+ indexes: [{ index: initialIndex }, { index: 'tsdb_index_2', create: true, mode: 'tsdb' }],
},
];
- function runTestsForEachScenario(
- initialIndex: string,
- testingFn: (
- indexes: Array<{
- index: string;
- create?: boolean;
- downsample?: boolean;
- tsdb?: boolean;
- removeTSDBFields?: boolean;
- }>
- ) => void
- ): void {
- for (const { name, indexes } of getScenarios(initialIndex)) {
- describe(name, () => {
- let dataViewName: string;
- let downsampledTargetIndex: string = '';
-
- before(async () => {
- for (const { index, create, downsample, tsdb, removeTSDBFields } of indexes) {
- if (create) {
- if (tsdb) {
- await dataStreams.createDataStream(
- index,
- getDataMapping({ tsdb, removeTSDBFields }),
- tsdb
- );
- } else {
- log.info(`creating a index "${index}" with mapping...`);
- await es.indices.create({
- index,
- mappings: {
- properties: getDataMapping({ tsdb: Boolean(tsdb), removeTSDBFields }),
- },
- });
- }
- // add data to the newly created index
- await createDocs(
- index,
- { isStream: Boolean(tsdb), removeTSDBFields },
- fromTimeForScenarios
- );
- }
- if (downsample) {
- downsampledTargetIndex = await dataStreams.downsampleTSDBIndex(index, {
- isStream: Boolean(tsdb),
- });
- }
- }
- dataViewName = `${indexes.map(({ index }) => index).join(',')}${
- downsampledTargetIndex ? `,${downsampledTargetIndex}` : ''
- }`;
- log.info(`creating a data view for "${dataViewName}"...`);
- await indexPatterns.create(
- {
- title: dataViewName,
- timeFieldName: '@timestamp',
- },
- { override: true }
- );
- await PageObjects.common.navigateToApp('lens');
- await elasticChart.setNewChartUiDebugFlag(true);
- // go to the
- await PageObjects.lens.goToTimeRange(
- fromTimeForScenarios,
- moment
- .utc(toTimeForScenarios, TIME_PICKER_FORMAT)
- .add(2, 'hour')
- .format(TIME_PICKER_FORMAT) // consider also new documents
- );
- });
-
- after(async () => {
- for (const { index, create, tsdb } of indexes) {
- if (create) {
- if (tsdb) {
- await dataStreams.deleteDataStream(index);
- } else {
- log.info(`deleting the index "${index}"...`);
- await es.indices.delete({
- index,
- });
- }
- }
- // no need to cleant he specific downsample index as everything linked to the stream
- // is cleaned up automatically
- }
- });
-
- beforeEach(async () => {
- await PageObjects.lens.switchDataPanelIndexPattern(dataViewName);
- await PageObjects.lens.removeLayer();
- });
-
- testingFn(indexes);
- });
- }
- }
+ const { runTestsForEachScenario, toTimeForScenarios, fromTimeForScenarios } =
+ setupScenarioRunner(getService, getPageObjects, getScenarios);
describe('Data-stream upgraded to TSDB scenarios', () => {
const streamIndex = 'data_stream';
@@ -755,7 +369,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
before(async () => {
log.info(`Creating "${streamIndex}" data stream...`);
- await dataStreams.createDataStream(streamIndex, getDataMapping(), false);
+ await dataStreams.createDataStream(
+ streamIndex,
+ getDataMapping({ mode: 'tsdb' }),
+ undefined
+ );
// add some data to the stream
await createDocs(streamIndex, { isStream: true }, fromTimeForScenarios);
@@ -767,8 +385,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
log.info(`Upgrade "${streamIndex}" stream to TSDB...`);
- const tsdbMapping = getDataMapping({ tsdb: true });
- await dataStreams.upgradeStreamToTSDB(streamIndex, tsdbMapping);
+ const tsdbMapping = getDataMapping({ mode: 'tsdb' });
+ await dataStreams.upgradeStream(streamIndex, tsdbMapping, 'tsdb');
log.info(
`Add more data to new "${streamConvertedToTsdbIndex}" dataView (now with TSDB backing index)...`
);
@@ -780,15 +398,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dataStreams.deleteDataStream(streamIndex);
});
- runTestsForEachScenario(streamConvertedToTsdbIndex, (indexes) => {
+ runTestsForEachScenario(streamConvertedToTsdbIndex, 'tsdb', (indexes) => {
it('should detect the data stream has now been upgraded to TSDB', async () => {
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'min',
field: `bytes_counter`,
@@ -800,53 +418,53 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
timeout: 500,
})
).to.eql(false);
- await PageObjects.lens.closeDimensionEditor();
+ await lens.closeDimensionEditor();
});
it(`should visualize a date histogram chart for counter field`, async () => {
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
// check the counter field works
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'min',
field: `bytes_counter`,
});
// and also that the count of documents should be "indexes.length" times overall
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'count',
});
- await PageObjects.lens.waitForVisualization('xyVisChart');
- const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
- const counterBars = data.bars![0].bars;
- const countBars = data.bars![1].bars;
+ await lens.waitForVisualization('xyVisChart');
+ const data = await lens.getCurrentChartDebugState('xyVisChart');
+ const counterBars = data?.bars![0].bars;
+ const countBars = data?.bars![1].bars;
log.info('Check counter data before the upgrade');
// check there's some data before the upgrade
- expect(counterBars[0].y).to.eql(5000);
+ expect(counterBars?.[0].y).to.eql(5000);
log.info('Check counter data after the upgrade');
// check there's some data after the upgrade
- expect(counterBars[counterBars.length - 1].y).to.eql(5000);
+ expect(counterBars?.[counterBars.length - 1].y).to.eql(5000);
// due to the flaky nature of exact check here, we're going to relax it
// as long as there's data before and after it is ok
log.info('Check count before the upgrade');
- const columnsToCheck = countBars.length / 2;
+ const columnsToCheck = countBars ? countBars.length / 2 : 0;
// Before the upgrade the count is N times the indexes
expect(sumFirstNValues(columnsToCheck, countBars)).to.be.greaterThan(
indexes.length * TEST_DOC_COUNT - 1
);
log.info('Check count after the upgrade');
// later there are only documents for the upgraded stream
- expect(sumFirstNValues(columnsToCheck, [...countBars].reverse())).to.be.greaterThan(
- TEST_DOC_COUNT - 1
- );
+ expect(
+ sumFirstNValues(columnsToCheck, [...(countBars ?? [])].reverse())
+ ).to.be.greaterThan(TEST_DOC_COUNT - 1);
});
});
});
@@ -858,7 +476,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
before(async () => {
log.info(`Creating "${tsdbStream}" data stream...`);
- await dataStreams.createDataStream(tsdbStream, getDataMapping({ tsdb: true }), true);
+ await dataStreams.createDataStream(tsdbStream, getDataMapping({ mode: 'tsdb' }), 'tsdb');
// add some data to the stream
await createDocs(tsdbStream, { isStream: true }, fromTimeForScenarios);
@@ -872,7 +490,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
`Dowgrade "${tsdbStream}" stream into regular stream "${tsdbConvertedToStream}"...`
);
- await dataStreams.downgradeTSDBtoStream(tsdbStream, getDataMapping({ tsdb: true }));
+ await dataStreams.downgradeStream(tsdbStream, getDataMapping({ mode: 'tsdb' }), 'tsdb');
log.info(`Add more data to new "${tsdbConvertedToStream}" dataView (no longer TSDB)...`);
// add some more data when upgraded
await createDocs(tsdbConvertedToStream, { isStream: true }, toTimeForScenarios);
@@ -882,15 +500,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dataStreams.deleteDataStream(tsdbConvertedToStream);
});
- runTestsForEachScenario(tsdbConvertedToStream, (indexes) => {
+ runTestsForEachScenario(tsdbConvertedToStream, 'tsdb', (indexes) => {
it('should keep TSDB restrictions only if a tsdb stream is in the dataView mix', async () => {
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'min',
field: `bytes_counter`,
@@ -901,28 +519,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
testSubjects.exists(`lns-indexPatternDimension-average incompatible`, {
timeout: 500,
})
- ).to.eql(indexes.some(({ tsdb }) => tsdb));
- await PageObjects.lens.closeDimensionEditor();
+ ).to.eql(indexes.some(({ mode }) => mode === 'tsdb'));
+ await lens.closeDimensionEditor();
});
it(`should visualize a date histogram chart for counter field`, async () => {
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
// just check the data is shown
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'count',
});
+ await lens.waitForVisualization('xyVisChart');
+ const data = await lens.getCurrentChartDebugState('xyVisChart');
+ const bars = data?.bars![0].bars;
+ const columnsToCheck = bars ? bars.length / 2 : 0;
// due to the flaky nature of exact check here, we're going to relax it
// as long as there's data before and after it is ok
- await PageObjects.lens.waitForVisualization('xyVisChart');
- const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
- const bars = data.bars![0].bars;
- const columnsToCheck = bars.length / 2;
log.info('Check count before the downgrade');
// Before the upgrade the count is N times the indexes
expect(sumFirstNValues(columnsToCheck, bars)).to.be.greaterThan(
@@ -930,14 +548,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
log.info('Check count after the downgrade');
// later there are only documents for the upgraded stream
- expect(sumFirstNValues(columnsToCheck, [...bars].reverse())).to.be.greaterThan(
+ expect(sumFirstNValues(columnsToCheck, [...(bars ?? [])].reverse())).to.be.greaterThan(
TEST_DOC_COUNT - 1
);
});
it('should visualize data when moving the time window around the downgrade moment', async () => {
// check after the downgrade
- await PageObjects.lens.goToTimeRange(
+ await lens.goToTimeRange(
moment
.utc(fromTimeForScenarios, TIME_PICKER_FORMAT)
.subtract(1, 'hour')
@@ -948,23 +566,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
.format(TIME_PICKER_FORMAT) // consider only new documents
);
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
- await PageObjects.lens.configureDimension({
+ await lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'count',
});
- await PageObjects.lens.waitForVisualization('xyVisChart');
- const dataBefore = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
- const barsBefore = dataBefore.bars![0].bars;
- expect(barsBefore.some(({ y }) => y)).to.eql(true);
+ await lens.waitForVisualization('xyVisChart');
+ const dataBefore = await lens.getCurrentChartDebugState('xyVisChart');
+ const barsBefore = dataBefore?.bars![0].bars;
+ expect(barsBefore?.some(({ y }) => y)).to.eql(true);
// check after the downgrade
- await PageObjects.lens.goToTimeRange(
+ await lens.goToTimeRange(
moment
.utc(toTimeForScenarios, TIME_PICKER_FORMAT)
.add(1, 'second')
@@ -975,10 +593,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
.format(TIME_PICKER_FORMAT) // consider also new documents
);
- await PageObjects.lens.waitForVisualization('xyVisChart');
- const dataAfter = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
- const barsAfter = dataAfter.bars![0].bars;
- expect(barsAfter.some(({ y }) => y)).to.eql(true);
+ await lens.waitForVisualization('xyVisChart');
+ const dataAfter = await lens.getCurrentChartDebugState('xyVisChart');
+ const barsAfter = dataAfter?.bars![0].bars;
+ expect(barsAfter?.some(({ y }) => y)).to.eql(true);
});
});
});
diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/tsdb_logsdb_helpers.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/tsdb_logsdb_helpers.ts
new file mode 100644
index 0000000000000..23822aa1395a9
--- /dev/null
+++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/tsdb_logsdb_helpers.ts
@@ -0,0 +1,480 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import expect from '@kbn/expect';
+import { Client } from '@elastic/elasticsearch';
+import { MappingProperty } from '@elastic/elasticsearch/lib/api/types';
+import { ToolingLog } from '@kbn/tooling-log';
+import moment from 'moment';
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+
+export const TEST_DOC_COUNT = 100;
+export const TIME_PICKER_FORMAT = 'MMM D, YYYY [@] HH:mm:ss.SSS';
+export const timeSeriesMetrics: Record = {
+ bytes_gauge: 'gauge',
+ bytes_counter: 'counter',
+};
+export const timeSeriesDimensions = ['request', 'url'];
+export const logsDBSpecialFields = ['host'];
+
+export const sharedESArchive =
+ 'test/functional/fixtures/es_archiver/kibana_sample_data_logs_logsdb';
+export const fromTime = 'Apr 16, 2023 @ 00:00:00.000';
+export const toTime = 'Jun 16, 2023 @ 00:00:00.000';
+
+export type TestDoc = Record>;
+
+export function testDocTemplate(mode: 'tsdb' | 'logsdb'): TestDoc {
+ return {
+ agent: 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1',
+ bytes: 6219,
+ clientip: '223.87.60.27',
+ extension: 'deb',
+ geo: {
+ srcdest: 'US:US',
+ src: 'US',
+ dest: 'US',
+ coordinates: { lat: 39.41042861, lon: -88.8454325 },
+ },
+ host: mode === 'tsdb' ? 'artifacts.elastic.co' : { name: 'artifacts.elastic.co' },
+ index: 'kibana_sample_data_logs',
+ ip: '223.87.60.27',
+ machine: { ram: 8589934592, os: 'win 8' },
+ memory: null,
+ message:
+ '223.87.60.27 - - [2018-07-22T00:39:02.912Z] "GET /elasticsearch/elasticsearch-6.3.2.deb_1 HTTP/1.1" 200 6219 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1"',
+ phpmemory: null,
+ referer: 'http://twitter.com/success/wendy-lawrence',
+ request: '/elasticsearch/elasticsearch-6.3.2.deb',
+ response: 200,
+ tags: ['success', 'info'],
+ '@timestamp': '2018-07-22T00:39:02.912Z',
+ url: 'https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.3.2.deb_1',
+ utc_time: '2018-07-22T00:39:02.912Z',
+ event: { dataset: 'sample_web_logs' },
+ bytes_gauge: 0,
+ bytes_counter: 0,
+ };
+}
+
+export function getDataMapping({
+ mode,
+ removeTSDBFields,
+ removeLogsDBFields,
+}: {
+ mode: 'tsdb' | 'logsdb';
+ removeTSDBFields?: boolean;
+ removeLogsDBFields?: boolean;
+}): Record {
+ const dataStreamMapping: Record = {
+ '@timestamp': {
+ type: 'date',
+ },
+ agent: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ bytes: {
+ type: 'long',
+ },
+ bytes_counter: {
+ type: 'long',
+ },
+ bytes_gauge: {
+ type: 'long',
+ },
+ clientip: {
+ type: 'ip',
+ },
+ event: {
+ properties: {
+ dataset: {
+ type: 'keyword',
+ },
+ },
+ },
+ extension: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ geo: {
+ properties: {
+ coordinates: {
+ type: 'geo_point',
+ },
+ dest: {
+ type: 'keyword',
+ },
+ src: {
+ type: 'keyword',
+ },
+ srcdest: {
+ type: 'keyword',
+ },
+ },
+ },
+ host:
+ mode === 'tsdb'
+ ? {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ }
+ : {
+ properties: {
+ name: {
+ type: 'keyword',
+ },
+ },
+ },
+ index: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ ip: {
+ type: 'ip',
+ },
+ machine: {
+ properties: {
+ os: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ ram: {
+ type: 'long',
+ },
+ },
+ },
+ memory: {
+ type: 'double',
+ },
+ message: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ phpmemory: {
+ type: 'long',
+ },
+ referer: {
+ type: 'keyword',
+ },
+ request: {
+ type: 'keyword',
+ },
+ response: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ tags: {
+ fields: {
+ keyword: {
+ ignore_above: 256,
+ type: 'keyword',
+ },
+ },
+ type: 'text',
+ },
+ timestamp: {
+ path: '@timestamp',
+ type: 'alias',
+ },
+ url: {
+ type: 'keyword',
+ },
+ utc_time: {
+ type: 'date',
+ },
+ };
+
+ if (mode === 'tsdb') {
+ // augment the current mapping
+ for (const [fieldName, fieldMapping] of Object.entries(dataStreamMapping || {})) {
+ if (
+ timeSeriesMetrics[fieldName] &&
+ (fieldMapping.type === 'double' || fieldMapping.type === 'long')
+ ) {
+ fieldMapping.time_series_metric = timeSeriesMetrics[fieldName];
+ }
+
+ if (timeSeriesDimensions.includes(fieldName) && fieldMapping.type === 'keyword') {
+ fieldMapping.time_series_dimension = true;
+ }
+ }
+ }
+ if (removeTSDBFields) {
+ for (const fieldName of Object.keys(timeSeriesMetrics)) {
+ delete dataStreamMapping[fieldName];
+ }
+ }
+ if (removeLogsDBFields) {
+ for (const fieldName of logsDBSpecialFields) {
+ delete dataStreamMapping[fieldName];
+ }
+ }
+ return dataStreamMapping;
+}
+
+export function sumFirstNValues(n: number, bars: Array<{ y: number }> | undefined): number {
+ const indexes = Array(n)
+ .fill(1)
+ .map((_, i) => i);
+ let countSum = 0;
+ for (const index of indexes) {
+ if (bars?.[index]) {
+ countSum += bars[index].y;
+ }
+ }
+ return countSum;
+}
+
+export const getDocsGenerator =
+ (log: ToolingLog, es: Client, mode: 'tsdb' | 'logsdb') =>
+ async (
+ esIndex: string,
+ {
+ isStream,
+ removeTSDBFields,
+ removeLogsDBFields,
+ }: { isStream: boolean; removeTSDBFields?: boolean; removeLogsDBFields?: boolean },
+ startTime: string
+ ) => {
+ log.info(
+ `Adding ${TEST_DOC_COUNT} to ${esIndex} with starting time from ${moment
+ .utc(startTime, TIME_PICKER_FORMAT)
+ .format(TIME_PICKER_FORMAT)} to ${moment
+ .utc(startTime, TIME_PICKER_FORMAT)
+ .add(2 * TEST_DOC_COUNT, 'seconds')
+ .format(TIME_PICKER_FORMAT)}`
+ );
+ const docs = Array(TEST_DOC_COUNT)
+ .fill(testDocTemplate(mode))
+ .map((templateDoc, i) => {
+ const timestamp = moment
+ .utc(startTime, TIME_PICKER_FORMAT)
+ .add(TEST_DOC_COUNT + i, 'seconds')
+ .format();
+ const doc: TestDoc = {
+ ...templateDoc,
+ '@timestamp': timestamp,
+ utc_time: timestamp,
+ bytes_gauge: Math.floor(Math.random() * 10000 * i),
+ bytes_counter: 5000,
+ };
+ if (removeTSDBFields) {
+ for (const field of Object.keys(timeSeriesMetrics)) {
+ delete doc[field];
+ }
+ }
+ // do not remove the fields for logsdb - ignore the flag
+ return doc;
+ });
+
+ const result = await es.bulk(
+ {
+ index: esIndex,
+ body: docs.map((d) => `{"${isStream ? 'create' : 'index'}": {}}\n${JSON.stringify(d)}\n`),
+ },
+ { meta: true }
+ );
+
+ const res = result.body;
+
+ if (res.errors) {
+ const resultsWithErrors = res.items
+ .filter(({ index }) => index?.error)
+ .map(({ index }) => index?.error);
+ for (const error of resultsWithErrors) {
+ log.error(`Error: ${JSON.stringify(error)}`);
+ }
+ const [indexExists, dataStreamExists] = await Promise.all([
+ es.indices.exists({ index: esIndex }),
+ es.indices.getDataStream({ name: esIndex }),
+ ]);
+ log.debug(`Index exists: ${indexExists} - Data stream exists: ${dataStreamExists}`);
+ }
+ log.info(`Indexed ${res.items.length} test data docs.`);
+ };
+
+export interface ScenarioIndexes {
+ index: string;
+ create?: boolean;
+ downsample?: boolean;
+ removeTSDBFields?: boolean;
+ removeLogsDBFields?: boolean;
+ mode?: 'tsdb' | 'logsdb';
+}
+type GetScenarioFn = (initialIndex: string) => Array<{
+ name: string;
+ indexes: ScenarioIndexes[];
+}>;
+
+export function setupScenarioRunner(
+ getService: FtrProviderContext['getService'],
+ getPageObjects: FtrProviderContext['getPageObjects'],
+ getScenario: GetScenarioFn
+) {
+ const now = moment().utc();
+ const fromMoment = now.clone().subtract(1, 'hour');
+ const toMoment = now.clone();
+ const fromTimeForScenarios = fromMoment.format(TIME_PICKER_FORMAT);
+ const toTimeForScenarios = toMoment.format(TIME_PICKER_FORMAT);
+
+ function runTestsForEachScenario(
+ initialIndex: string,
+ scenarioMode: 'tsdb' | 'logsdb',
+ testingFn: (indexes: ScenarioIndexes[]) => void
+ ): void {
+ const { common, lens } = getPageObjects(['common', 'lens', 'dashboard']);
+ const es = getService('es');
+ const log = getService('log');
+ const dataStreams = getService('dataStreams');
+ const elasticChart = getService('elasticChart');
+ const indexPatterns = getService('indexPatterns');
+ const createDocs = getDocsGenerator(log, es, scenarioMode);
+
+ for (const { name, indexes } of getScenario(initialIndex)) {
+ describe(name, () => {
+ let dataViewName: string;
+ let downsampledTargetIndex: string = '';
+
+ before(async () => {
+ for (const {
+ index,
+ create,
+ downsample,
+ mode,
+ removeTSDBFields,
+ removeLogsDBFields,
+ } of indexes) {
+ // Validate the scenario config
+ if (downsample && mode !== 'tsdb') {
+ expect().fail('Cannot create a scenario with downsampled stream without tsdb');
+ }
+ // Kick off the creation
+ const isStream = mode !== undefined;
+ if (create) {
+ if (isStream) {
+ await dataStreams.createDataStream(
+ index,
+ getDataMapping({
+ mode,
+ removeTSDBFields: Boolean(removeTSDBFields || mode === 'logsdb'),
+ removeLogsDBFields,
+ }),
+ mode
+ );
+ } else {
+ log.info(`creating a index "${index}" with mapping...`);
+ await es.indices.create({
+ index,
+ mappings: {
+ properties: getDataMapping({
+ mode: mode === 'logsdb' ? 'logsdb' : 'tsdb', // use tsdb by default in regular index is specified
+ removeTSDBFields,
+ removeLogsDBFields,
+ }),
+ },
+ });
+ }
+ // add data to the newly created index
+ await createDocs(
+ index,
+ { isStream, removeTSDBFields, removeLogsDBFields },
+ fromTimeForScenarios
+ );
+ }
+ if (downsample) {
+ downsampledTargetIndex = await dataStreams.downsampleTSDBIndex(index, {
+ isStream: mode === 'tsdb',
+ });
+ }
+ }
+ dataViewName = `${indexes.map(({ index }) => index).join(',')}${
+ downsampledTargetIndex ? `,${downsampledTargetIndex}` : ''
+ }`;
+ log.info(`creating a data view for "${dataViewName}"...`);
+ await indexPatterns.create(
+ {
+ title: dataViewName,
+ timeFieldName: '@timestamp',
+ },
+ { override: true }
+ );
+ await common.navigateToApp('lens');
+ await elasticChart.setNewChartUiDebugFlag(true);
+ // go to the
+ await lens.goToTimeRange(
+ fromTimeForScenarios,
+ moment
+ .utc(toTimeForScenarios, TIME_PICKER_FORMAT)
+ .add(2, 'hour')
+ .format(TIME_PICKER_FORMAT) // consider also new documents
+ );
+ });
+
+ after(async () => {
+ for (const { index, create, mode: indexMode } of indexes) {
+ if (create) {
+ if (indexMode === 'tsdb' || indexMode === 'logsdb') {
+ await dataStreams.deleteDataStream(index);
+ } else {
+ log.info(`deleting the index "${index}"...`);
+ await es.indices.delete({
+ index,
+ });
+ }
+ }
+ // no need to cleant he specific downsample index as everything linked to the stream
+ // is cleaned up automatically
+ }
+ });
+
+ beforeEach(async () => {
+ await lens.switchDataPanelIndexPattern(dataViewName);
+ await lens.removeLayer();
+ });
+
+ testingFn(indexes);
+ });
+ }
+ }
+
+ return { runTestsForEachScenario, fromTimeForScenarios, toTimeForScenarios };
+}
diff --git a/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless.ts b/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless.ts
index 948a418279ac9..692ae096265fb 100644
--- a/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless.ts
+++ b/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless.ts
@@ -6,8 +6,10 @@
*/
import { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants';
+import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils';
import { createTestConfig } from '../../config.base';
+// TODO: Remove the agentless default config once Serverless API is merged and default policy is deleted
export default createTestConfig({
serverlessProject: 'security',
junit: {
@@ -16,13 +18,23 @@ export default createTestConfig({
kbnServerArgs: [
`--xpack.fleet.packages.0.name=cloud_security_posture`,
`--xpack.fleet.packages.0.version=${CLOUD_CREDENTIALS_PACKAGE_VERSION}`,
+ `--xpack.fleet.agentless.enabled=true`,
+ `--xpack.fleet.agents.fleet_server.hosts=["https://ftr.kibana:8220"]`,
+ `--xpack.fleet.internal.fleetServerStandalone=true`,
- // Agentless Configuration based on Serverless Security Dev Yaml - config/serverless.security.dev.yml
- `--xpack.fleet.enableExperimental.0=agentless`,
+ // Agentless Configuration based on Serverless Default policy`,
`--xpack.fleet.agentPolicies.0.id=agentless`,
`--xpack.fleet.agentPolicies.0.name=agentless`,
`--xpack.fleet.agentPolicies.0.package_policies=[]`,
`--xpack.cloud.serverless.project_id=some_fake_project_id`,
+ `--xpack.fleet.agentPolicies.0.is_default=true`,
+ `--xpack.fleet.agentPolicies.0.is_default_fleet_server=true`,
+
+ // Serverless Agentless API
+ `--xpack.fleet.agentless.api.url=http://localhost:8089`,
+ `--xpack.fleet.agentless.api.tls.certificate=${KBN_CERT_PATH}`,
+ `--xpack.fleet.agentless.api.tls.key=${KBN_KEY_PATH}`,
+ `--xpack.fleet.agentless.api.tls.ca=${CA_CERT_PATH}`,
],
// load tests in the index file
testFiles: [require.resolve('./ftr/cloud_security_posture/agentless')],
diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts
index 6adbbac3cdc57..90991304936ea 100644
--- a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts
+++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts
@@ -6,9 +6,11 @@
*/
import { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants';
import expect from '@kbn/expect';
-
+import * as http from 'http';
import type { FtrProviderContext } from '../../../../../ftr_provider_context';
+import { setupMockServer } from '../agentless_api/mock_agentless_api';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
+ const mockAgentlessApiService = setupMockServer();
const pageObjects = getPageObjects([
'settings',
'common',
@@ -24,9 +26,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
let cisIntegration: typeof pageObjects.cisAddIntegration;
let cisIntegrationAws: typeof pageObjects.cisAddIntegration.cisAws;
let testSubjectIds: typeof pageObjects.cisAddIntegration.testSubjectIds;
+ let mockApiServer: http.Server;
const previousPackageVersion = '1.9.0';
before(async () => {
+ mockApiServer = mockAgentlessApiService.listen(8089);
await pageObjects.svlCommonPage.loginAsAdmin();
cisIntegration = pageObjects.cisAddIntegration;
cisIntegrationAws = pageObjects.cisAddIntegration.cisAws;
@@ -41,6 +45,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
.set('kbn-xsrf', 'xxxx')
.send({ force: true })
.expect(200);
+ mockApiServer.close();
});
describe('Serverless - Agentless CIS_AWS Single Account Launch Cloud formation', () => {
@@ -110,7 +115,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
});
- describe('Serverless - Agentless CIS_AWS edit flow', () => {
+ // TODO: Migrate test after Serverless default agentless policy is deleted.
+ describe.skip('Serverless - Agentless CIS_AWS edit flow', () => {
it(`user should save and edit agentless integration policy`, async () => {
const newDirectAccessKeyId = `newDirectAccessKey`;
@@ -142,7 +148,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
).to.be('true');
});
});
-
// FLAKY: https://github.com/elastic/kibana/issues/191017
describe.skip('Serverless - Agentless CIS_AWS Create flow', () => {
it(`user should save agentless integration policy when there are no api or validation errors and button is not disabled`, async () => {
diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts
index cd9e5b2168d1a..85a45f67bf9cc 100644
--- a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts
+++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts
@@ -7,7 +7,9 @@
import expect from '@kbn/expect';
import { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants';
+import * as http from 'http';
import type { FtrProviderContext } from '../../../../../ftr_provider_context';
+import { setupMockServer } from '../agentless_api/mock_agentless_api';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const pageObjects = getPageObjects(['common', 'svlCommonPage', 'cisAddIntegration', 'header']);
@@ -21,7 +23,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
let cisIntegrationGcp: typeof pageObjects.cisAddIntegration.cisGcp;
let testSubjectIds: typeof pageObjects.cisAddIntegration.testSubjectIds;
+ const mockAgentlessApiService = setupMockServer();
+ let mockApiServer: http.Server;
+
before(async () => {
+ mockApiServer = mockAgentlessApiService.listen(8089);
await pageObjects.svlCommonPage.loginAsAdmin();
cisIntegration = pageObjects.cisAddIntegration;
cisIntegrationGcp = pageObjects.cisAddIntegration.cisGcp;
@@ -36,6 +42,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
.set('kbn-xsrf', 'xxxx')
.send({ force: true })
.expect(200);
+ mockApiServer.close();
});
describe('Agentless CIS_GCP Single Account Launch Cloud shell', () => {
@@ -93,7 +100,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
});
- describe('Serverless - Agentless CIS_GCP edit flow', () => {
+ describe.skip('Serverless - Agentless CIS_GCP edit flow', () => {
it(`user should save and edit agentless integration policy`, async () => {
const newCredentialsJSON = 'newJson';
await cisIntegration.createAgentlessIntegration({
diff --git a/yarn.lock b/yarn.lock
index acedd70e165ec..ac4dfd08d254d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2590,287 +2590,299 @@
dependencies:
is-negated-glob "^1.0.0"
-"@hapi/accept@^5.0.1", "@hapi/accept@^5.0.2":
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523"
- integrity sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw==
+"@hapi/accept@^6.0.1", "@hapi/accept@^6.0.3":
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-6.0.3.tgz#eef0800a4f89cd969da8e5d0311dc877c37279ab"
+ integrity sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==
dependencies:
- "@hapi/boom" "9.x.x"
- "@hapi/hoek" "9.x.x"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/hoek" "^11.0.2"
-"@hapi/ammo@5.x.x", "@hapi/ammo@^5.0.1":
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/@hapi/ammo/-/ammo-5.0.1.tgz#9d34560f5c214eda563d838c01297387efaab490"
- integrity sha512-FbCNwcTbnQP4VYYhLNGZmA76xb2aHg9AMPiy18NZyWMG310P5KdFGyA9v2rm5ujrIny77dEEIkMOwl0Xv+fSSA==
+"@hapi/ammo@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/ammo/-/ammo-6.0.1.tgz#1bc9f7102724ff288ca03b721854fc5393ad123a"
+ integrity sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==
dependencies:
- "@hapi/hoek" "9.x.x"
+ "@hapi/hoek" "^11.0.2"
-"@hapi/b64@5.x.x":
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/@hapi/b64/-/b64-5.0.0.tgz#b8210cbd72f4774985e78569b77e97498d24277d"
- integrity sha512-ngu0tSEmrezoiIaNGG6rRvKOUkUuDdf4XTPnONHGYfSGRmDqPZX5oJL6HAdKTo1UQHECbdB4OzhWrfgVppjHUw==
+"@hapi/b64@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/b64/-/b64-6.0.1.tgz#786b47dc070e14465af49e2428c1025bd06ed3df"
+ integrity sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==
dependencies:
- "@hapi/hoek" "9.x.x"
+ "@hapi/hoek" "^11.0.2"
-"@hapi/boom@9.x.x", "@hapi/boom@^9.0.0", "@hapi/boom@^9.1.0", "@hapi/boom@^9.1.4":
- version "9.1.4"
- resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.4.tgz#1f9dad367c6a7da9f8def24b4a986fc5a7bd9db6"
- integrity sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==
+"@hapi/boom@^10.0.0", "@hapi/boom@^10.0.1":
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.1.tgz#ebb14688275ae150aa6af788dbe482e6a6062685"
+ integrity sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==
dependencies:
- "@hapi/hoek" "9.x.x"
+ "@hapi/hoek" "^11.0.2"
-"@hapi/bounce@2.x.x", "@hapi/bounce@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@hapi/bounce/-/bounce-2.0.0.tgz#e6ef56991c366b1e2738b2cd83b01354d938cf3d"
- integrity sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A==
+"@hapi/bounce@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/bounce/-/bounce-3.0.1.tgz#25a51bf95733749c557c6bf948048bffa66435e4"
+ integrity sha512-G+/Pp9c1Ha4FDP+3Sy/Xwg2O4Ahaw3lIZFSX+BL4uWi64CmiETuZPxhKDUD4xBMOUZbBlzvO8HjiK8ePnhBadA==
dependencies:
- "@hapi/boom" "9.x.x"
- "@hapi/hoek" "9.x.x"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/hoek" "^11.0.2"
-"@hapi/bourne@2.x.x":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d"
- integrity sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg==
+"@hapi/bourne@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-3.0.0.tgz#f11fdf7dda62fe8e336fa7c6642d9041f30356d7"
+ integrity sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==
-"@hapi/call@^8.0.0":
- version "8.0.1"
- resolved "https://registry.yarnpkg.com/@hapi/call/-/call-8.0.1.tgz#9e64cd8ba6128eb5be6e432caaa572b1ed8cd7c0"
- integrity sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g==
+"@hapi/call@^9.0.1":
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/call/-/call-9.0.1.tgz#569b87d5b67abf0e58fb82a3894a61aaed3ca92e"
+ integrity sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==
dependencies:
- "@hapi/boom" "9.x.x"
- "@hapi/hoek" "9.x.x"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/hoek" "^11.0.2"
-"@hapi/catbox-memory@^5.0.0":
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/@hapi/catbox-memory/-/catbox-memory-5.0.0.tgz#6c18dad1a80737480d1c33bfbefd5d028deec86d"
- integrity sha512-ByuxVJPHNaXwLzbBv4GdTr6ccpe1nG+AfYt+8ftDWEJY7EWBWzD+Klhy5oPTDGzU26pNUh1e7fcYI1ILZRxAXQ==
+"@hapi/catbox-memory@^6.0.2":
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/@hapi/catbox-memory/-/catbox-memory-6.0.2.tgz#399fa83e85134d45a548eee978e4c3c1523e1a70"
+ integrity sha512-H1l4ugoFW/ZRkqeFrIo8p1rWN0PA4MDTfu4JmcoNDvnY975o29mqoZblqFTotxNHlEkMPpIiIBJTV+Mbi+aF0g==
dependencies:
- "@hapi/boom" "9.x.x"
- "@hapi/hoek" "9.x.x"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/hoek" "^11.0.2"
-"@hapi/catbox@^11.1.1":
- version "11.1.1"
- resolved "https://registry.yarnpkg.com/@hapi/catbox/-/catbox-11.1.1.tgz#d277e2d5023fd69cddb33d05b224ea03065fec0c"
- integrity sha512-u/8HvB7dD/6X8hsZIpskSDo4yMKpHxFd7NluoylhGrL6cUfYxdQPnvUp9YU2C6F9hsyBVLGulBd9vBN1ebfXOQ==
+"@hapi/catbox@^12.1.1":
+ version "12.1.1"
+ resolved "https://registry.yarnpkg.com/@hapi/catbox/-/catbox-12.1.1.tgz#9339dca0a5b18b3ca0a825ac5dfc916dbc5bab83"
+ integrity sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==
dependencies:
- "@hapi/boom" "9.x.x"
- "@hapi/hoek" "9.x.x"
- "@hapi/podium" "4.x.x"
- "@hapi/validate" "1.x.x"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/podium" "^5.0.0"
+ "@hapi/validate" "^2.0.1"
-"@hapi/content@^5.0.2":
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/@hapi/content/-/content-5.0.2.tgz#ae57954761de570392763e64cdd75f074176a804"
- integrity sha512-mre4dl1ygd4ZyOH3tiYBrOUBzV7Pu/EOs8VLGf58vtOEECWed8Uuw6B4iR9AN/8uQt42tB04qpVaMyoMQh0oMw==
+"@hapi/content@^6.0.0":
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/content/-/content-6.0.0.tgz#2427af3bac8a2f743512fce2a70cbdc365af29df"
+ integrity sha512-CEhs7j+H0iQffKfe5Htdak5LBOz/Qc8TRh51cF+BFv0qnuph3Em4pjGVzJMkI2gfTDdlJKWJISGWS1rK34POGA==
dependencies:
- "@hapi/boom" "9.x.x"
+ "@hapi/boom" "^10.0.0"
-"@hapi/cookie@^11.0.2":
- version "11.0.2"
- resolved "https://registry.yarnpkg.com/@hapi/cookie/-/cookie-11.0.2.tgz#7169c060157a3541146b976e5f0ca9b3f7577d7f"
- integrity sha512-LRpSuHC53urzml83c5eUHSPPt7YtK1CaaPZU9KmnhZlacVVojrWJzOUIcwOADDvCZjDxowCO3zPMaOqzEm9kgg==
+"@hapi/cookie@^12.0.1":
+ version "12.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/cookie/-/cookie-12.0.1.tgz#7e4da17f9843a01732a5aa875592c1aa8c6bed05"
+ integrity sha512-TpykARUIgTBvgbsgtYe5CrM/7XBGms2/22JD3N6dzct6bG1vcOC6pH1JWIos1O5zvQnyDSJ2pnaFk+DToHkAng==
dependencies:
- "@hapi/boom" "9.x.x"
- "@hapi/bounce" "2.x.x"
- "@hapi/hoek" "9.x.x"
- "@hapi/validate" "1.x.x"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/bounce" "^3.0.1"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/validate" "^2.0.1"
-"@hapi/cryptiles@5.x.x":
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/@hapi/cryptiles/-/cryptiles-5.1.0.tgz#655de4cbbc052c947f696148c83b187fc2be8f43"
- integrity sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==
+"@hapi/cryptiles@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/cryptiles/-/cryptiles-6.0.1.tgz#7868a9d4233567ed66f0a9caf85fdcc56e980621"
+ integrity sha512-9GM9ECEHfR8lk5ASOKG4+4ZsEzFqLfhiryIJ2ISePVB92OHLp/yne4m+zn7z9dgvM98TLpiFebjDFQ0UHcqxXQ==
dependencies:
- "@hapi/boom" "9.x.x"
-
-"@hapi/file@2.x.x":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@hapi/file/-/file-2.0.0.tgz#2ecda37d1ae9d3078a67c13b7da86e8c3237dfb9"
- integrity sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ==
+ "@hapi/boom" "^10.0.1"
-"@hapi/h2o2@^9.1.0":
- version "9.1.0"
- resolved "https://registry.yarnpkg.com/@hapi/h2o2/-/h2o2-9.1.0.tgz#b223f4978b6f2b0d7d9db10a84a567606c4c3551"
- integrity sha512-B7E58bMhxmpiDI22clxTexoAaVShNBk1Ez6S8SQjQZu5FxxD6Tqa44sXeZQBtWrdJF7ZRbsY60/C8AHLRxagNA==
- dependencies:
- "@hapi/boom" "9.x.x"
- "@hapi/hoek" "9.x.x"
- "@hapi/validate" "1.x.x"
- "@hapi/wreck" "17.x.x"
-
-"@hapi/hapi@^20.2.2":
- version "20.2.2"
- resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-20.2.2.tgz#5810efbf5c0aad367932e86d4066d82ac817e98c"
- integrity sha512-crhU6TIKt7QsksWLYctDBAXogk9PYAm7UzdpETyuBHC2pCa6/+B5NykiOVLG/3FCIgHo/raPVtan8bYtByHORQ==
- dependencies:
- "@hapi/accept" "^5.0.1"
- "@hapi/ammo" "^5.0.1"
- "@hapi/boom" "^9.1.0"
- "@hapi/bounce" "^2.0.0"
- "@hapi/call" "^8.0.0"
- "@hapi/catbox" "^11.1.1"
- "@hapi/catbox-memory" "^5.0.0"
- "@hapi/heavy" "^7.0.1"
- "@hapi/hoek" "^9.0.4"
- "@hapi/mimos" "^6.0.0"
- "@hapi/podium" "^4.1.1"
- "@hapi/shot" "^5.0.5"
- "@hapi/somever" "^3.0.0"
- "@hapi/statehood" "^7.0.4"
- "@hapi/subtext" "^7.0.3"
- "@hapi/teamwork" "^5.1.1"
- "@hapi/topo" "^5.0.0"
- "@hapi/validate" "^1.1.1"
-
-"@hapi/heavy@^7.0.1":
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/@hapi/heavy/-/heavy-7.0.1.tgz#73315ae33b6e7682a0906b7a11e8ca70e3045874"
- integrity sha512-vJ/vzRQ13MtRzz6Qd4zRHWS3FaUc/5uivV2TIuExGTM9Qk+7Zzqj0e2G7EpE6KztO9SalTbiIkTh7qFKj/33cA==
+"@hapi/file@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/file/-/file-3.0.0.tgz#f1fd824493ac89a6fceaf89c824afc5ae2121c09"
+ integrity sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==
+
+"@hapi/h2o2@^10.0.4":
+ version "10.0.4"
+ resolved "https://registry.yarnpkg.com/@hapi/h2o2/-/h2o2-10.0.4.tgz#9255328f851fcc6e647af68d5e0f464523a19133"
+ integrity sha512-dvD8+Y/Okc0fh0blqaYCLIrcy0+1LqIhMr7hjk8elLQZ9mkw2hKFB9dFKuRfWf+1nvHpGlW+PwccqkdebynQbg==
+ dependencies:
+ "@hapi/boom" "^10.0.1"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/validate" "^2.0.1"
+ "@hapi/wreck" "^18.0.1"
+
+"@hapi/hapi@^21.1.0", "@hapi/hapi@^21.3.10":
+ version "21.3.10"
+ resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-21.3.10.tgz#0357db7ca49415e50e5df80ba50ad3964f2a62f3"
+ integrity sha512-CmEcmTREW394MaGGKvWpoOK4rG8tKlpZLs30tbaBzhCrhiL2Ti/HARek9w+8Ya4nMBGcd+kDAzvU44OX8Ms0Jg==
+ dependencies:
+ "@hapi/accept" "^6.0.1"
+ "@hapi/ammo" "^6.0.1"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/bounce" "^3.0.1"
+ "@hapi/call" "^9.0.1"
+ "@hapi/catbox" "^12.1.1"
+ "@hapi/catbox-memory" "^6.0.2"
+ "@hapi/heavy" "^8.0.1"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/mimos" "^7.0.1"
+ "@hapi/podium" "^5.0.1"
+ "@hapi/shot" "^6.0.1"
+ "@hapi/somever" "^4.1.1"
+ "@hapi/statehood" "^8.1.1"
+ "@hapi/subtext" "^8.1.0"
+ "@hapi/teamwork" "^6.0.0"
+ "@hapi/topo" "^6.0.1"
+ "@hapi/validate" "^2.0.1"
+
+"@hapi/heavy@^8.0.1":
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/heavy/-/heavy-8.0.1.tgz#e2be4a6a249005b5a587f7604aafa8ed02461fb6"
+ integrity sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==
dependencies:
- "@hapi/boom" "9.x.x"
- "@hapi/hoek" "9.x.x"
- "@hapi/validate" "1.x.x"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/validate" "^2.0.1"
+
+"@hapi/hoek@^11.0.2", "@hapi/hoek@^11.0.4":
+ version "11.0.4"
+ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.4.tgz#42a7f244fd3dd777792bfb74b8c6340ae9182f37"
+ integrity sha512-PnsP5d4q7289pS2T2EgGz147BFJ2Jpb4yrEdkpz2IhgEUzos1S7HTl7ezWh1yfYzYlj89KzLdCRkqsP6SIryeQ==
-"@hapi/hoek@9.x.x", "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.0.4", "@hapi/hoek@^9.2.1", "@hapi/hoek@^9.3.0":
+"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==
-"@hapi/inert@^6.0.4":
- version "6.0.4"
- resolved "https://registry.yarnpkg.com/@hapi/inert/-/inert-6.0.4.tgz#0544221eabc457110a426818358d006e70ff1f41"
- integrity sha512-tpmNqtCCAd+5Ts07bJmMaA79+ZUIf0zSWnQMaWtbcO4nGrO/yXB2AzoslfzFX2JEV9vGeF3FfL8mYw0pHl8VGg==
+"@hapi/inert@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@hapi/inert/-/inert-7.1.0.tgz#0bcebe4c5115230f9d7b5d6e8c3bf799ca91958e"
+ integrity sha512-5X+cl/Ozm0U9uPGGX1dSKhnhTQIf161bH/kkTN9OBVAZKFG+nrj8j/NMj6S1zBBZWmQrkVRNPfCUGrXzB4fCFQ==
+ dependencies:
+ "@hapi/ammo" "^6.0.1"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/bounce" "^3.0.1"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/validate" "^2.0.1"
+ lru-cache "^7.14.1"
+
+"@hapi/iron@^7.0.1":
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/iron/-/iron-7.0.1.tgz#f74bace8dad9340c7c012c27c078504f070f14b5"
+ integrity sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==
dependencies:
- "@hapi/ammo" "5.x.x"
- "@hapi/boom" "9.x.x"
- "@hapi/bounce" "2.x.x"
- "@hapi/hoek" "9.x.x"
- "@hapi/validate" "1.x.x"
- lru-cache "^6.0.0"
+ "@hapi/b64" "^6.0.1"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/bourne" "^3.0.0"
+ "@hapi/cryptiles" "^6.0.1"
+ "@hapi/hoek" "^11.0.2"
-"@hapi/iron@6.x.x", "@hapi/iron@^6.0.0":
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/@hapi/iron/-/iron-6.0.0.tgz#ca3f9136cda655bdd6028de0045da0de3d14436f"
- integrity sha512-zvGvWDufiTGpTJPG1Y/McN8UqWBu0k/xs/7l++HVU535NLHXsHhy54cfEMdW7EjwKfbBfM9Xy25FmTiobb7Hvw==
+"@hapi/mimos@^7.0.1":
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-7.0.1.tgz#5b65c76bb9da28ba34b0092215891f2c72bc899d"
+ integrity sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==
dependencies:
- "@hapi/b64" "5.x.x"
- "@hapi/boom" "9.x.x"
- "@hapi/bourne" "2.x.x"
- "@hapi/cryptiles" "5.x.x"
- "@hapi/hoek" "9.x.x"
+ "@hapi/hoek" "^11.0.2"
+ mime-db "^1.52.0"
-"@hapi/mimos@^6.0.0":
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-6.0.0.tgz#daa523d9c07222c7e8860cb7c9c5501fd6506484"
- integrity sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg==
+"@hapi/nigel@^5.0.1":
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/nigel/-/nigel-5.0.1.tgz#a6dfe357e9d48d944e2ffc552bd95cb701d79ee9"
+ integrity sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==
dependencies:
- "@hapi/hoek" "9.x.x"
- mime-db "1.x.x"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/vise" "^5.0.1"
-"@hapi/nigel@4.x.x":
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/@hapi/nigel/-/nigel-4.0.2.tgz#8f84ef4bca4fb03b2376463578f253b0b8e863c4"
- integrity sha512-ht2KoEsDW22BxQOEkLEJaqfpoKPXxi7tvabXy7B/77eFtOyG5ZEstfZwxHQcqAiZhp58Ae5vkhEqI03kawkYNw==
+"@hapi/pez@^6.1.0":
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-6.1.0.tgz#64d9f95580fc7d8f1d13437ee4a8676709954fda"
+ integrity sha512-+FE3sFPYuXCpuVeHQ/Qag1b45clR2o54QoonE/gKHv9gukxQ8oJJZPR7o3/ydDTK6racnCJXxOyT1T93FCJMIg==
dependencies:
- "@hapi/hoek" "^9.0.4"
- "@hapi/vise" "^4.0.0"
+ "@hapi/b64" "^6.0.1"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/content" "^6.0.0"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/nigel" "^5.0.1"
-"@hapi/pez@^5.0.1":
- version "5.0.3"
- resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-5.0.3.tgz#b75446e6fef8cbb16816573ab7da1b0522e7a2a1"
- integrity sha512-mpikYRJjtrbJgdDHG/H9ySqYqwJ+QU/D7FXsYciS9P7NYBXE2ayKDAy3H0ou6CohOCaxPuTV4SZ0D936+VomHA==
+"@hapi/podium@^5.0.0", "@hapi/podium@^5.0.1":
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-5.0.1.tgz#f292b4c0ca3118747394a102c6c3340bda96662f"
+ integrity sha512-eznFTw6rdBhAijXFIlBOMJJd+lXTvqbrBIS4Iu80r2KTVIo4g+7fLy4NKp/8+UnSt5Ox6mJtAlKBU/Sf5080TQ==
dependencies:
- "@hapi/b64" "5.x.x"
- "@hapi/boom" "9.x.x"
- "@hapi/content" "^5.0.2"
- "@hapi/hoek" "9.x.x"
- "@hapi/nigel" "4.x.x"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/teamwork" "^6.0.0"
+ "@hapi/validate" "^2.0.1"
-"@hapi/podium@4.x.x", "@hapi/podium@^4.1.1", "@hapi/podium@^4.1.3":
- version "4.1.3"
- resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.3.tgz#91e20838fc2b5437f511d664aabebbb393578a26"
- integrity sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g==
+"@hapi/shot@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-6.0.1.tgz#ea84d1810b7c8599d5517c23b4ec55a529d7dc16"
+ integrity sha512-s5ynMKZXYoDd3dqPw5YTvOR/vjHvMTxc388+0qL0jZZP1+uwXuUD32o9DuuuLsmTlyXCWi02BJl1pBpwRuUrNA==
dependencies:
- "@hapi/hoek" "9.x.x"
- "@hapi/teamwork" "5.x.x"
- "@hapi/validate" "1.x.x"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/validate" "^2.0.1"
-"@hapi/shot@^5.0.5":
- version "5.0.5"
- resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-5.0.5.tgz#a25c23d18973bec93c7969c51bf9579632a5bebd"
- integrity sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A==
+"@hapi/somever@^4.1.1":
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/@hapi/somever/-/somever-4.1.1.tgz#b492c78408303c72cd1a39c5060f35d18a404b27"
+ integrity sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==
dependencies:
- "@hapi/hoek" "9.x.x"
- "@hapi/validate" "1.x.x"
+ "@hapi/bounce" "^3.0.1"
+ "@hapi/hoek" "^11.0.2"
-"@hapi/somever@^3.0.0":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@hapi/somever/-/somever-3.0.0.tgz#f4e9b16a948415b926b4dd898013602b0cb45758"
- integrity sha512-Upw/kmKotC9iEmK4y047HMYe4LDKsE5NWfjgX41XNKmFvxsQL7OiaCWVhuyyhU0ShDGBfIAnCH8jZr49z/JzZA==
- dependencies:
- "@hapi/bounce" "2.x.x"
- "@hapi/hoek" "9.x.x"
-
-"@hapi/statehood@^7.0.4":
- version "7.0.4"
- resolved "https://registry.yarnpkg.com/@hapi/statehood/-/statehood-7.0.4.tgz#6acb9d0817b5c657089356f7d9fd60af0bce4f41"
- integrity sha512-Fia6atroOVmc5+2bNOxF6Zv9vpbNAjEXNcUbWXavDqhnJDlchwUUwKS5LCi5mGtCTxRhUKKHwuxuBZJkmLZ7fw==
- dependencies:
- "@hapi/boom" "9.x.x"
- "@hapi/bounce" "2.x.x"
- "@hapi/bourne" "2.x.x"
- "@hapi/cryptiles" "5.x.x"
- "@hapi/hoek" "9.x.x"
- "@hapi/iron" "6.x.x"
- "@hapi/validate" "1.x.x"
-
-"@hapi/subtext@^7.0.3":
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/@hapi/subtext/-/subtext-7.0.3.tgz#f7440fc7c966858e1f39681e99eb6171c71e7abd"
- integrity sha512-CekDizZkDGERJ01C0+TzHlKtqdXZxzSWTOaH6THBrbOHnsr3GY+yiMZC+AfNCypfE17RaIakGIAbpL2Tk1z2+A==
- dependencies:
- "@hapi/boom" "9.x.x"
- "@hapi/bourne" "2.x.x"
- "@hapi/content" "^5.0.2"
- "@hapi/file" "2.x.x"
- "@hapi/hoek" "9.x.x"
- "@hapi/pez" "^5.0.1"
- "@hapi/wreck" "17.x.x"
-
-"@hapi/teamwork@5.x.x", "@hapi/teamwork@^5.1.1":
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/@hapi/teamwork/-/teamwork-5.1.1.tgz#4d2ba3cac19118a36c44bf49a3a47674de52e4e4"
- integrity sha512-1oPx9AE5TIv+V6Ih54RP9lTZBso3rP8j4Xhb6iSVwPXtAM+sDopl5TFMv5Paw73UnpZJ9gjcrTE1BXrWt9eQrg==
+"@hapi/statehood@^8.1.1":
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/@hapi/statehood/-/statehood-8.1.1.tgz#db4bd14c90810a1389763cb0b0b8f221aa4179c1"
+ integrity sha512-YbK7PSVUA59NArAW5Np0tKRoIZ5VNYUicOk7uJmWZF6XyH5gGL+k62w77SIJb0AoAJ0QdGQMCQ/WOGL1S3Ydow==
+ dependencies:
+ "@hapi/boom" "^10.0.1"
+ "@hapi/bounce" "^3.0.1"
+ "@hapi/bourne" "^3.0.0"
+ "@hapi/cryptiles" "^6.0.1"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/iron" "^7.0.1"
+ "@hapi/validate" "^2.0.1"
+
+"@hapi/subtext@^8.1.0":
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/@hapi/subtext/-/subtext-8.1.0.tgz#58733020a6655bc4d978df9e2f75e31696ff3f91"
+ integrity sha512-PyaN4oSMtqPjjVxLny1k0iYg4+fwGusIhaom9B2StinBclHs7v46mIW706Y+Wo21lcgulGyXbQrmT/w4dus6ww==
+ dependencies:
+ "@hapi/boom" "^10.0.1"
+ "@hapi/bourne" "^3.0.0"
+ "@hapi/content" "^6.0.0"
+ "@hapi/file" "^3.0.0"
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/pez" "^6.1.0"
+ "@hapi/wreck" "^18.0.1"
+
+"@hapi/teamwork@^6.0.0":
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/teamwork/-/teamwork-6.0.0.tgz#b3a173cf811ba59fc6ee22318a1b51f4561f06e0"
+ integrity sha512-05HumSy3LWfXpmJ9cr6HzwhAavrHkJ1ZRCmNE2qJMihdM5YcWreWPfyN0yKT2ZjCM92au3ZkuodjBxOibxM67A==
-"@hapi/topo@^5.0.0", "@hapi/topo@^5.1.0":
+"@hapi/topo@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012"
integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==
dependencies:
"@hapi/hoek" "^9.0.0"
-"@hapi/validate@1.x.x", "@hapi/validate@^1.1.1", "@hapi/validate@^1.1.3":
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-1.1.3.tgz#f750a07283929e09b51aa16be34affb44e1931ad"
- integrity sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA==
+"@hapi/topo@^6.0.1":
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-6.0.2.tgz#f219c1c60da8430228af4c1f2e40c32a0d84bbb4"
+ integrity sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==
dependencies:
- "@hapi/hoek" "^9.0.0"
- "@hapi/topo" "^5.0.0"
+ "@hapi/hoek" "^11.0.2"
-"@hapi/vise@^4.0.0":
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/@hapi/vise/-/vise-4.0.0.tgz#c6a94fe121b94a53bf99e7489f7fcc74c104db02"
- integrity sha512-eYyLkuUiFZTer59h+SGy7hUm+qE9p+UemePTHLlIWppEd+wExn3Df5jO04bFQTm7nleF5V8CtuYQYb+VFpZ6Sg==
+"@hapi/validate@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-2.0.1.tgz#45cf228c4c8cfc61ba2da7e0a5ba93ff3b9afff1"
+ integrity sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==
+ dependencies:
+ "@hapi/hoek" "^11.0.2"
+ "@hapi/topo" "^6.0.1"
+
+"@hapi/vise@^5.0.1":
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/vise/-/vise-5.0.1.tgz#5c9f16bcf1c039ddd4b6cad5f32d71eeb6bb7dac"
+ integrity sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==
dependencies:
- "@hapi/hoek" "9.x.x"
+ "@hapi/hoek" "^11.0.2"
-"@hapi/wreck@17.x.x", "@hapi/wreck@^17.0.0", "@hapi/wreck@^17.1.0":
- version "17.1.0"
- resolved "https://registry.yarnpkg.com/@hapi/wreck/-/wreck-17.1.0.tgz#fbdc380c6f3fa1f8052dc612b2d3b6ce3e88dbec"
- integrity sha512-nx6sFyfqOpJ+EFrHX+XWwJAxs3ju4iHdbB/bwR8yTNZOiYmuhA8eCe7lYPtYmb4j7vyK/SlbaQsmTtUrMvPEBw==
+"@hapi/wreck@^18.0.1", "@hapi/wreck@^18.1.0":
+ version "18.1.0"
+ resolved "https://registry.yarnpkg.com/@hapi/wreck/-/wreck-18.1.0.tgz#68e631fc7568ebefc6252d5b86cb804466c8dbe6"
+ integrity sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==
dependencies:
- "@hapi/boom" "9.x.x"
- "@hapi/bourne" "2.x.x"
- "@hapi/hoek" "9.x.x"
+ "@hapi/boom" "^10.0.1"
+ "@hapi/bourne" "^3.0.0"
+ "@hapi/hoek" "^11.0.2"
"@hello-pangea/dnd@16.6.0", "@hello-pangea/dnd@^16.6.0":
version "16.6.0"
@@ -5883,6 +5895,10 @@
version "0.0.0"
uid ""
+"@kbn/observability-synthetics-test-data@link:x-pack/packages/observability/synthetics_test_data":
+ version "0.0.0"
+ uid ""
+
"@kbn/observability-utils@link:x-pack/packages/observability/observability_utils":
version "0.0.0"
uid ""
@@ -10533,63 +10549,14 @@
"@types/vinyl-fs" "*"
chokidar "^2.1.2"
-"@types/hapi__catbox@*":
- version "10.2.3"
- resolved "https://registry.yarnpkg.com/@types/hapi__catbox/-/hapi__catbox-10.2.3.tgz#c9279c16d709bf2987491c332e11d18124ae018f"
- integrity sha512-gs6MKMKXzWpSqeYsPaDIDAxD8jLNg7aFxgAJE6Jnc+ns072Z9fuh39/NF5gSk1KNoGCLnIpeZ0etT9gY9QDCKg==
-
-"@types/hapi__cookie@^10.1.3":
- version "10.1.3"
- resolved "https://registry.yarnpkg.com/@types/hapi__cookie/-/hapi__cookie-10.1.3.tgz#b0ab2be28669e083c63253927262c43f24395c2c"
- integrity sha512-v/hPXxOVfBdkTa+S4cGec88vZjvEbLaZp8xjg2MtjDhykx1/mLtY4EJHk6fI1cW5WGgFV9pgMjz5mOktjNwILw==
- dependencies:
- "@types/hapi__hapi" "*"
- joi "^17.3.0"
-
-"@types/hapi__h2o2@^8.3.3":
- version "8.3.3"
- resolved "https://registry.yarnpkg.com/@types/hapi__h2o2/-/hapi__h2o2-8.3.3.tgz#f6c5ac480a6fd421025f7d0f78dfa916703511b7"
- integrity sha512-+qWZVFVGc5Y0wuNZvVe876VJjUBCJ8eQdXovg4Rg9laHpeERQejluI7aw31xXWfLojTuHz3ThZzC6Orqras05Q==
- dependencies:
- "@hapi/boom" "^9.0.0"
- "@hapi/wreck" "^17.0.0"
- "@types/hapi__hapi" "*"
- "@types/node" "*"
-
-"@types/hapi__hapi@*", "@types/hapi__hapi@^20.0.9":
- version "20.0.9"
- resolved "https://registry.yarnpkg.com/@types/hapi__hapi/-/hapi__hapi-20.0.9.tgz#9d570846c96268266a14c970c13aeeaccfc8e172"
- integrity sha512-fGpKScknCKZityRXdZgpCLGbm41R1ppFgnKHerfZlqOOlCX/jI129S6ghgBqkqCE8m9A0CIu1h7Ch04lD9KOoA==
- dependencies:
- "@hapi/boom" "^9.0.0"
- "@hapi/iron" "^6.0.0"
- "@hapi/podium" "^4.1.3"
- "@types/hapi__catbox" "*"
- "@types/hapi__mimos" "*"
- "@types/hapi__shot" "*"
- "@types/node" "*"
- joi "^17.3.0"
-
-"@types/hapi__inert@^5.2.3":
- version "5.2.3"
- resolved "https://registry.yarnpkg.com/@types/hapi__inert/-/hapi__inert-5.2.3.tgz#f586eb240d5997c9968d1b4e8b37679517045ca1"
- integrity sha512-I1mWQrEc7oMqGtofT0rwBgRBCBurz0wNzbq8QZsHWR+aXM0bk1j9GA6zwyGIeO53PNl2C1c2kpXlc084xCV+Tg==
- dependencies:
- "@types/hapi__hapi" "*"
-
-"@types/hapi__mimos@*":
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/@types/hapi__mimos/-/hapi__mimos-4.1.0.tgz#47dbf89ebfc05183c1de2797e9426793db9a0d85"
- integrity sha512-hcdSoYa32wcP+sEfyf85ieGwElwokcZ/mma8eyqQ4OTHeCAGwfaoiGxjG4z1Dm+RGhIYLHlW54ji5FFwahH12A==
- dependencies:
- "@types/mime-db" "*"
-
-"@types/hapi__shot@*":
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/@types/hapi__shot/-/hapi__shot-4.1.1.tgz#c760322b90eb77f36a3003a442e8dc69e6ae3922"
- integrity sha512-44Jj7jJAFgNVgfdbyVtBUbEIbYqWRKAbLR4kiQxBbVEdf8ZKfa5Hg1qg4QdzXBgjw0mopewU4wx1/eWRTVelNQ==
+"@types/hapi__cookie@^12.0.5":
+ version "12.0.5"
+ resolved "https://registry.yarnpkg.com/@types/hapi__cookie/-/hapi__cookie-12.0.5.tgz#b28e885043d1951721ed4d8ef2087943b8b8267d"
+ integrity sha512-rZNcJRDuutoSLHVHNSoIACabeGZEhjYFKqdW9vmucpUgE50YfPbtGPREzXEKOf2/1RR6d+jrSkNRwxUEaUIfsQ==
dependencies:
+ "@hapi/hapi" "^21.1.0"
"@types/node" "*"
+ joi "^17.7.0"
"@types/has-ansi@^3.0.0":
version "3.0.0"
@@ -10876,11 +10843,6 @@
dependencies:
"@types/braces" "*"
-"@types/mime-db@*":
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/@types/mime-db/-/mime-db-1.27.0.tgz#9bc014a1fd1fdf47649c1a54c6dd7966b8284792"
- integrity sha1-m8AUof0f30dknBpUxt15ZrgoR5I=
-
"@types/mime-types@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.0.tgz#9ca52cda363f699c69466c2a6ccdaad913ea7a73"
@@ -13613,13 +13575,13 @@ brfs@^2.0.0, brfs@^2.0.2:
static-module "^3.0.2"
through2 "^2.0.0"
-brok@^5.0.2:
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/brok/-/brok-5.0.2.tgz#b77e7203ce89d30939a5b877a9bb3acb4dffc848"
- integrity sha512-mqsoOGPjcP9oltC8dD4PnRCiJREmFg+ee588mVYZgZNd8YV5Zo6eOLv/fp6HxdYffaxvkKfPHjc+sRWIkuIu7A==
+brok@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/brok/-/brok-6.0.0.tgz#82f081de7a180802c224955cb6c2888b81a27b28"
+ integrity sha512-z5ND+K0+Go4Doiq8GtEMuBicbbky2DSbNzrRqEnOFYJgqrSu85nK2HsczCRwd7UznHTSUVYEkuEpRm5HtfaCSw==
dependencies:
- "@hapi/hoek" "^9.0.4"
- "@hapi/validate" "^1.1.3"
+ "@hapi/hoek" "^11.0.4"
+ "@hapi/validate" "^2.0.1"
brorand@^1.0.1, brorand@^1.1.0:
version "1.1.0"
@@ -21637,7 +21599,7 @@ joi-to-json@^4.3.0:
lodash "^4.17.21"
semver-compare "^1.0.0"
-joi@^17.13.3, joi@^17.3.0:
+joi@^17.13.3, joi@^17.7.0:
version "17.13.3"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec"
integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==
@@ -23271,11 +23233,16 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-mime-db@1.51.0, mime-db@1.x.x, "mime-db@>= 1.40.0 < 2":
+mime-db@1.51.0:
version "1.51.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==
+"mime-db@>= 1.40.0 < 2", mime-db@^1.52.0:
+ version "1.53.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447"
+ integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==
+
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.34"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24"