From be65efcc473825fe64c6d86b2c7401050a65cffe Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Mon, 18 Oct 2021 20:19:53 -0400
Subject: [PATCH 01/18] [Fleet, App search] Add App Search ingestion methods to
the unified integrations view (#115433) (#115463)
* Add a new category for Web crawler
* Add App Search integrations
* Fix isBeta flag for Web Crawler
It's already GA
Co-authored-by: Vadim Yakhin
---
.../custom_integrations/common/index.ts | 1 +
.../enterprise_search/server/integrations.ts | 63 +++++++++++++++++++
2 files changed, 64 insertions(+)
diff --git a/src/plugins/custom_integrations/common/index.ts b/src/plugins/custom_integrations/common/index.ts
index 944ac6ba3e6ee..98148bb22c816 100755
--- a/src/plugins/custom_integrations/common/index.ts
+++ b/src/plugins/custom_integrations/common/index.ts
@@ -49,6 +49,7 @@ export const INTEGRATION_CATEGORY_DISPLAY = {
project_management: 'Project Management',
software_development: 'Software Development',
upload_file: 'Upload a file',
+ website_search: 'Website Search',
};
/**
diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts
index 48909261243e8..eee5cdc3aaec3 100644
--- a/x-pack/plugins/enterprise_search/server/integrations.ts
+++ b/x-pack/plugins/enterprise_search/server/integrations.ts
@@ -301,4 +301,67 @@ export const registerEnterpriseSearchIntegrations = (
...integration,
});
});
+
+ customIntegrations.registerCustomIntegration({
+ id: 'app_search_web_crawler',
+ title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.webCrawlerName', {
+ defaultMessage: 'Web Crawler',
+ }),
+ description: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.integrations.webCrawlerDescription',
+ {
+ defaultMessage: "Add search to your website with App Search's web crawler.",
+ }
+ ),
+ categories: ['website_search'],
+ uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=crawler',
+ icons: [
+ {
+ type: 'eui',
+ src: 'globe',
+ },
+ ],
+ shipper: 'enterprise_search',
+ isBeta: false,
+ });
+
+ customIntegrations.registerCustomIntegration({
+ id: 'app_search_json',
+ title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.jsonName', {
+ defaultMessage: 'JSON',
+ }),
+ description: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.jsonDescription', {
+ defaultMessage: 'Search over your JSON data with App Search.',
+ }),
+ categories: ['upload_file'],
+ uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=json',
+ icons: [
+ {
+ type: 'eui',
+ src: 'exportAction',
+ },
+ ],
+ shipper: 'enterprise_search',
+ isBeta: false,
+ });
+
+ customIntegrations.registerCustomIntegration({
+ id: 'app_search_api',
+ title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.apiName', {
+ defaultMessage: 'API',
+ }),
+ description: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.apiDescription', {
+ defaultMessage: "Add search to your application with App Search's robust APIs.",
+ }),
+ categories: ['custom'],
+ uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=api',
+ icons: [
+ {
+ type: 'eui',
+ src: 'editorCodeBlock',
+ },
+ ],
+ shipper: 'enterprise_search',
+ isBeta: false,
+ });
};
From 5840c438a816b54b483b79d4184c23b3148ffcf4 Mon Sep 17 00:00:00 2001
From: Walter Rafelsberger
Date: Tue, 19 Oct 2021 02:41:31 +0200
Subject: [PATCH 02/18] [ML] APM Correlations: Get trace samples tab overall
distribution via APM endpoint. (#114615) (#115466)
This creates an APM API endpoint that fetches data for the latency distribution chart in the trace samples tab on the transactions page. Previously, this data was fetched via the custom Kibana search strategies used for APM Correlations which causes issues in load balancing setups.
---
.../failed_transactions_correlations/types.ts | 13 +-
.../latency_correlations/types.ts | 12 +-
.../apm/common/search_strategies/types.ts | 12 +-
.../latency_correlations.test.tsx | 5 +-
.../distribution/index.test.tsx | 59 +++------
.../distribution/index.tsx | 79 +++++++++---
.../apm/public/hooks/use_search_strategy.ts | 10 +-
.../get_overall_latency_distribution.ts | 121 ++++++++++++++++++
.../latency/get_percentile_threshold_value.ts | 53 ++++++++
.../plugins/apm/server/lib/latency/types.ts | 23 ++++
...ransactions_correlations_search_service.ts | 31 ++---
.../failed_transactions_correlations/index.ts | 6 +-
.../latency_correlations/index.ts | 6 +-
.../latency_correlations_search_service.ts | 31 ++---
.../field_stats/get_field_stats.test.ts | 4 +-
.../queries/get_query_with_params.test.ts | 12 +-
.../queries/get_query_with_params.ts | 17 +--
.../queries/get_request_base.test.ts | 4 +
.../queries/query_correlation.test.ts | 4 +-
.../queries/query_field_candidates.test.ts | 4 +-
.../queries/query_field_value_pairs.test.ts | 4 +-
.../queries/query_fractions.test.ts | 4 +-
.../queries/query_histogram.test.ts | 4 +-
.../query_histogram_range_steps.test.ts | 4 +-
.../queries/query_histogram_range_steps.ts | 6 +-
.../query_histograms_generator.test.ts | 4 +-
.../queries/query_percentiles.test.ts | 4 +-
.../queries/query_ranges.test.ts | 4 +-
.../search_strategy_provider.test.ts | 4 +-
.../search_strategy_provider.ts | 104 +++++++++++----
.../get_global_apm_server_route_repository.ts | 2 +
.../apm/server/routes/latency_distribution.ts | 63 +++++++++
.../tests/correlations/failed_transactions.ts | 4 +-
.../tests/correlations/latency.ts | 23 ++--
.../test/apm_api_integration/tests/index.ts | 4 +
.../latency_overall_distribution.ts | 65 ++++++++++
36 files changed, 590 insertions(+), 219 deletions(-)
create mode 100644 x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts
create mode 100644 x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts
create mode 100644 x-pack/plugins/apm/server/lib/latency/types.ts
create mode 100644 x-pack/plugins/apm/server/routes/latency_distribution.ts
create mode 100644 x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts
diff --git a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts
index 266d7246c35d4..28ce2ff24b961 100644
--- a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts
+++ b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts
@@ -5,12 +5,7 @@
* 2.0.
*/
-import {
- FieldValuePair,
- HistogramItem,
- RawResponseBase,
- SearchStrategyClientParams,
-} from '../types';
+import { FieldValuePair, HistogramItem } from '../types';
import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from './constants';
import { FieldStats } from '../field_stats_types';
@@ -33,11 +28,7 @@ export interface FailedTransactionsCorrelationsParams {
percentileThreshold: number;
}
-export type FailedTransactionsCorrelationsRequestParams =
- FailedTransactionsCorrelationsParams & SearchStrategyClientParams;
-
-export interface FailedTransactionsCorrelationsRawResponse
- extends RawResponseBase {
+export interface FailedTransactionsCorrelationsRawResponse {
log: string[];
failedTransactionsCorrelations?: FailedTransactionsCorrelation[];
percentileThresholdValue?: number;
diff --git a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts
index 2eb2b37159459..ea74175a3dacb 100644
--- a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts
+++ b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts
@@ -5,12 +5,7 @@
* 2.0.
*/
-import {
- FieldValuePair,
- HistogramItem,
- RawResponseBase,
- SearchStrategyClientParams,
-} from '../types';
+import { FieldValuePair, HistogramItem } from '../types';
import { FieldStats } from '../field_stats_types';
export interface LatencyCorrelation extends FieldValuePair {
@@ -33,10 +28,7 @@ export interface LatencyCorrelationsParams {
analyzeCorrelations: boolean;
}
-export type LatencyCorrelationsRequestParams = LatencyCorrelationsParams &
- SearchStrategyClientParams;
-
-export interface LatencyCorrelationsRawResponse extends RawResponseBase {
+export interface LatencyCorrelationsRawResponse {
log: string[];
overallHistogram?: HistogramItem[];
percentileThresholdValue?: number;
diff --git a/x-pack/plugins/apm/common/search_strategies/types.ts b/x-pack/plugins/apm/common/search_strategies/types.ts
index d7c6eab1f07c1..ff925f70fc9b0 100644
--- a/x-pack/plugins/apm/common/search_strategies/types.ts
+++ b/x-pack/plugins/apm/common/search_strategies/types.ts
@@ -31,16 +31,26 @@ export interface RawResponseBase {
took: number;
}
-export interface SearchStrategyClientParams {
+export interface SearchStrategyClientParamsBase {
environment: string;
kuery: string;
serviceName?: string;
transactionName?: string;
transactionType?: string;
+}
+
+export interface RawSearchStrategyClientParams
+ extends SearchStrategyClientParamsBase {
start?: string;
end?: string;
}
+export interface SearchStrategyClientParams
+ extends SearchStrategyClientParamsBase {
+ start: number;
+ end: number;
+}
+
export interface SearchStrategyServerParams {
index: string;
includeFrozen?: boolean;
diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx
index 9956452c565b3..918f94e64ef09 100644
--- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx
@@ -19,6 +19,7 @@ import type { IKibanaSearchResponse } from 'src/plugins/data/public';
import { EuiThemeProvider } from 'src/plugins/kibana_react/common';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
import type { LatencyCorrelationsRawResponse } from '../../../../common/search_strategies/latency_correlations/types';
+import type { RawResponseBase } from '../../../../common/search_strategies/types';
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context';
import {
@@ -34,7 +35,9 @@ function Wrapper({
dataSearchResponse,
}: {
children?: ReactNode;
- dataSearchResponse: IKibanaSearchResponse;
+ dataSearchResponse: IKibanaSearchResponse<
+ LatencyCorrelationsRawResponse & RawResponseBase
+ >;
}) {
const mockDataSearch = jest.fn(() => of(dataSearchResponse));
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx
index bd0ff4c87c3be..0e9639de4aa74 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx
@@ -8,43 +8,24 @@
import { render, screen, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React, { ReactNode } from 'react';
-import { of } from 'rxjs';
import { CoreStart } from 'kibana/public';
import { merge } from 'lodash';
-import { dataPluginMock } from 'src/plugins/data/public/mocks';
-import type { IKibanaSearchResponse } from 'src/plugins/data/public';
import { EuiThemeProvider } from 'src/plugins/kibana_react/common';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
-import type { LatencyCorrelationsRawResponse } from '../../../../../common/search_strategies/latency_correlations/types';
import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider';
import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context';
import {
mockApmPluginContextValue,
MockApmPluginContextWrapper,
} from '../../../../context/apm_plugin/mock_apm_plugin_context';
+import * as useFetcherModule from '../../../../hooks/use_fetcher';
import { fromQuery } from '../../../shared/Links/url_helpers';
import { getFormattedSelection, TransactionDistribution } from './index';
-function Wrapper({
- children,
- dataSearchResponse,
-}: {
- children?: ReactNode;
- dataSearchResponse: IKibanaSearchResponse;
-}) {
- const mockDataSearch = jest.fn(() => of(dataSearchResponse));
-
- const dataPluginMockStart = dataPluginMock.createStartContract();
+function Wrapper({ children }: { children?: ReactNode }) {
const KibanaReactContext = createKibanaReactContext({
- data: {
- ...dataPluginMockStart,
- search: {
- ...dataPluginMockStart.search,
- search: mockDataSearch,
- },
- },
usageCollection: { reportUiCounter: () => {} },
} as Partial);
@@ -105,18 +86,14 @@ describe('transaction_details/distribution', () => {
describe('TransactionDistribution', () => {
it('shows loading indicator when the service is running and returned no results yet', async () => {
+ jest.spyOn(useFetcherModule, 'useFetcher').mockImplementation(() => ({
+ data: {},
+ refetch: () => {},
+ status: useFetcherModule.FETCH_STATUS.LOADING,
+ }));
+
render(
-
+
{
});
it("doesn't show loading indicator when the service isn't running", async () => {
+ jest.spyOn(useFetcherModule, 'useFetcher').mockImplementation(() => ({
+ data: { percentileThresholdValue: 1234, overallHistogram: [] },
+ refetch: () => {},
+ status: useFetcherModule.FETCH_STATUS.SUCCESS,
+ }));
+
render(
-
+
{
+ if (serviceName && environment && start && end) {
+ return callApmApi({
+ endpoint: 'GET /internal/apm/latency/overall_distribution',
+ params: {
+ query: {
+ serviceName,
+ transactionName,
+ transactionType,
+ kuery,
+ environment,
+ start,
+ end,
+ percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD,
+ },
+ },
+ });
+ }
+ },
+ [
+ serviceName,
+ transactionName,
+ transactionType,
+ kuery,
+ environment,
+ start,
+ end,
+ ]
);
+ const overallHistogram =
+ data.overallHistogram === undefined && status !== FETCH_STATUS.LOADING
+ ? []
+ : data.overallHistogram;
+ const hasData =
+ Array.isArray(overallHistogram) && overallHistogram.length > 0;
+
useEffect(() => {
- if (isErrorMessage(progress.error)) {
+ if (isErrorMessage(error)) {
notifications.toasts.addDanger({
title: i18n.translate(
'xpack.apm.transactionDetails.distribution.errorTitle',
@@ -119,10 +156,10 @@ export function TransactionDistribution({
defaultMessage: 'An error occurred fetching the distribution',
}
),
- text: progress.error.toString(),
+ text: error.toString(),
});
}
- }, [progress.error, notifications.toasts]);
+ }, [error, notifications.toasts]);
const trackApmEvent = useUiTracker({ app: 'apm' });
@@ -213,7 +250,7 @@ export function TransactionDistribution({
data={transactionDistributionChartData}
markerCurrentTransaction={markerCurrentTransaction}
markerPercentile={DEFAULT_PERCENTILE_THRESHOLD}
- markerValue={response.percentileThresholdValue ?? 0}
+ markerValue={data.percentileThresholdValue ?? 0}
onChartSelection={onTrackedChartSelection as BrushEndListener}
hasData={hasData}
selection={selection}
diff --git a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts
index ca8d28b106f84..275eddb68ae00 100644
--- a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts
+++ b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts
@@ -16,7 +16,7 @@ import {
} from '../../../../../src/plugins/data/public';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
-import type { SearchStrategyClientParams } from '../../common/search_strategies/types';
+import type { RawSearchStrategyClientParams } from '../../common/search_strategies/types';
import type { RawResponseBase } from '../../common/search_strategies/types';
import type {
LatencyCorrelationsParams,
@@ -77,13 +77,15 @@ interface SearchStrategyReturnBase {
export function useSearchStrategy(
searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS,
searchStrategyParams: LatencyCorrelationsParams
-): SearchStrategyReturnBase;
+): SearchStrategyReturnBase;
// Function overload for Failed Transactions Correlations
export function useSearchStrategy(
searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS,
searchStrategyParams: FailedTransactionsCorrelationsParams
-): SearchStrategyReturnBase;
+): SearchStrategyReturnBase<
+ FailedTransactionsCorrelationsRawResponse & RawResponseBase
+>;
export function useSearchStrategy<
TRawResponse extends RawResponseBase,
@@ -145,7 +147,7 @@ export function useSearchStrategy<
// Submit the search request using the `data.search` service.
searchSubscription$.current = data.search
.search<
- IKibanaSearchRequest,
+ IKibanaSearchRequest,
IKibanaSearchResponse
>(request, {
strategy: searchStrategyName,
diff --git a/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts
new file mode 100644
index 0000000000000..39470869488c3
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts
@@ -0,0 +1,121 @@
+/*
+ * 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 { estypes } from '@elastic/elasticsearch';
+
+import { ProcessorEvent } from '../../../common/processor_event';
+
+import { withApmSpan } from '../../utils/with_apm_span';
+
+import {
+ getHistogramIntervalRequest,
+ getHistogramRangeSteps,
+} from '../search_strategies/queries/query_histogram_range_steps';
+import { getTransactionDurationRangesRequest } from '../search_strategies/queries/query_ranges';
+
+import { getPercentileThresholdValue } from './get_percentile_threshold_value';
+import type {
+ OverallLatencyDistributionOptions,
+ OverallLatencyDistributionResponse,
+} from './types';
+
+export async function getOverallLatencyDistribution(
+ options: OverallLatencyDistributionOptions
+) {
+ return withApmSpan('get_overall_latency_distribution', async () => {
+ const overallLatencyDistribution: OverallLatencyDistributionResponse = {
+ log: [],
+ };
+
+ const { setup, ...rawParams } = options;
+ const { apmEventClient } = setup;
+ const params = {
+ // pass on an empty index because we're using only the body attribute
+ // of the request body getters we're reusing from search strategies.
+ index: '',
+ ...rawParams,
+ };
+
+ // #1: get 95th percentile to be displayed as a marker in the log log chart
+ overallLatencyDistribution.percentileThresholdValue =
+ await getPercentileThresholdValue(options);
+
+ // finish early if we weren't able to identify the percentileThresholdValue.
+ if (!overallLatencyDistribution.percentileThresholdValue) {
+ return overallLatencyDistribution;
+ }
+
+ // #2: get histogram range steps
+ const steps = 100;
+
+ const { body: histogramIntervalRequestBody } =
+ getHistogramIntervalRequest(params);
+
+ const histogramIntervalResponse = (await apmEventClient.search(
+ 'get_histogram_interval',
+ {
+ // TODO: add support for metrics
+ apm: { events: [ProcessorEvent.transaction] },
+ body: histogramIntervalRequestBody,
+ }
+ )) as {
+ aggregations?: {
+ transaction_duration_min: estypes.AggregationsValueAggregate;
+ transaction_duration_max: estypes.AggregationsValueAggregate;
+ };
+ hits: { total: estypes.SearchTotalHits };
+ };
+
+ if (
+ !histogramIntervalResponse.aggregations ||
+ histogramIntervalResponse.hits.total.value === 0
+ ) {
+ return overallLatencyDistribution;
+ }
+
+ const min =
+ histogramIntervalResponse.aggregations.transaction_duration_min.value;
+ const max =
+ histogramIntervalResponse.aggregations.transaction_duration_max.value * 2;
+
+ const histogramRangeSteps = getHistogramRangeSteps(min, max, steps);
+
+ // #3: get histogram chart data
+ const { body: transactionDurationRangesRequestBody } =
+ getTransactionDurationRangesRequest(params, histogramRangeSteps);
+
+ const transactionDurationRangesResponse = (await apmEventClient.search(
+ 'get_transaction_duration_ranges',
+ {
+ // TODO: add support for metrics
+ apm: { events: [ProcessorEvent.transaction] },
+ body: transactionDurationRangesRequestBody,
+ }
+ )) as {
+ aggregations?: {
+ logspace_ranges: estypes.AggregationsMultiBucketAggregate<{
+ from: number;
+ doc_count: number;
+ }>;
+ };
+ };
+
+ if (!transactionDurationRangesResponse.aggregations) {
+ return overallLatencyDistribution;
+ }
+
+ overallLatencyDistribution.overallHistogram =
+ transactionDurationRangesResponse.aggregations.logspace_ranges.buckets
+ .map((d) => ({
+ key: d.from,
+ doc_count: d.doc_count,
+ }))
+ .filter((d) => d.key !== undefined);
+
+ return overallLatencyDistribution;
+ });
+}
diff --git a/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts
new file mode 100644
index 0000000000000..0d417a370e0b6
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { estypes } from '@elastic/elasticsearch';
+
+import { ProcessorEvent } from '../../../common/processor_event';
+
+import { getTransactionDurationPercentilesRequest } from '../search_strategies/queries/query_percentiles';
+
+import type { OverallLatencyDistributionOptions } from './types';
+
+export async function getPercentileThresholdValue(
+ options: OverallLatencyDistributionOptions
+) {
+ const { setup, percentileThreshold, ...rawParams } = options;
+ const { apmEventClient } = setup;
+ const params = {
+ // pass on an empty index because we're using only the body attribute
+ // of the request body getters we're reusing from search strategies.
+ index: '',
+ ...rawParams,
+ };
+
+ const { body: transactionDurationPercentilesRequestBody } =
+ getTransactionDurationPercentilesRequest(params, [percentileThreshold]);
+
+ const transactionDurationPercentilesResponse = (await apmEventClient.search(
+ 'get_transaction_duration_percentiles',
+ {
+ // TODO: add support for metrics
+ apm: { events: [ProcessorEvent.transaction] },
+ body: transactionDurationPercentilesRequestBody,
+ }
+ )) as {
+ aggregations?: {
+ transaction_duration_percentiles: estypes.AggregationsTDigestPercentilesAggregate;
+ };
+ };
+
+ if (!transactionDurationPercentilesResponse.aggregations) {
+ return;
+ }
+
+ const percentilesResponseThresholds =
+ transactionDurationPercentilesResponse.aggregations
+ .transaction_duration_percentiles?.values ?? {};
+
+ return percentilesResponseThresholds[`${percentileThreshold}.0`];
+}
diff --git a/x-pack/plugins/apm/server/lib/latency/types.ts b/x-pack/plugins/apm/server/lib/latency/types.ts
new file mode 100644
index 0000000000000..8dad1a39bd159
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/latency/types.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Setup } from '../helpers/setup_request';
+import { CorrelationsOptions } from '../search_strategies/queries/get_filters';
+
+export interface OverallLatencyDistributionOptions extends CorrelationsOptions {
+ percentileThreshold: number;
+ setup: Setup;
+}
+
+export interface OverallLatencyDistributionResponse {
+ log: string[];
+ percentileThresholdValue?: number;
+ overallHistogram?: Array<{
+ key: number;
+ doc_count: number;
+ }>;
+}
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts
index af5e535abdc3f..efc28ce98e5e0 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts
@@ -9,17 +9,15 @@ import { chunk } from 'lodash';
import type { ElasticsearchClient } from 'src/core/server';
-import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server';
-import {
- IKibanaSearchRequest,
- IKibanaSearchResponse,
-} from '../../../../../../../src/plugins/data/common';
-
import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames';
import { EventOutcome } from '../../../../common/event_outcome';
-import type { SearchStrategyServerParams } from '../../../../common/search_strategies/types';
import type {
- FailedTransactionsCorrelationsRequestParams,
+ SearchStrategyClientParams,
+ SearchStrategyServerParams,
+ RawResponseBase,
+} from '../../../../common/search_strategies/types';
+import type {
+ FailedTransactionsCorrelationsParams,
FailedTransactionsCorrelationsRawResponse,
} from '../../../../common/search_strategies/failed_transactions_correlations/types';
import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices';
@@ -38,22 +36,18 @@ import { failedTransactionsCorrelationsSearchServiceStateProvider } from './fail
import { ERROR_CORRELATION_THRESHOLD } from '../constants';
import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats';
-export type FailedTransactionsCorrelationsSearchServiceProvider =
+type FailedTransactionsCorrelationsSearchServiceProvider =
SearchServiceProvider<
- FailedTransactionsCorrelationsRequestParams,
- FailedTransactionsCorrelationsRawResponse
+ FailedTransactionsCorrelationsParams & SearchStrategyClientParams,
+ FailedTransactionsCorrelationsRawResponse & RawResponseBase
>;
-export type FailedTransactionsCorrelationsSearchStrategy = ISearchStrategy<
- IKibanaSearchRequest,
- IKibanaSearchResponse
->;
-
export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider =
(
esClient: ElasticsearchClient,
getApmIndices: () => Promise,
- searchServiceParams: FailedTransactionsCorrelationsRequestParams,
+ searchServiceParams: FailedTransactionsCorrelationsParams &
+ SearchStrategyClientParams,
includeFrozen: boolean
) => {
const { addLogMessage, getLogMessages } = searchServiceLogProvider();
@@ -63,7 +57,8 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact
async function fetchErrorCorrelations() {
try {
const indices = await getApmIndices();
- const params: FailedTransactionsCorrelationsRequestParams &
+ const params: FailedTransactionsCorrelationsParams &
+ SearchStrategyClientParams &
SearchStrategyServerParams = {
...searchServiceParams,
index: indices.transaction,
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts
index ec91165cb481b..4763cd994d309 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts
@@ -5,8 +5,4 @@
* 2.0.
*/
-export {
- failedTransactionsCorrelationsSearchServiceProvider,
- FailedTransactionsCorrelationsSearchServiceProvider,
- FailedTransactionsCorrelationsSearchStrategy,
-} from './failed_transactions_correlations_search_service';
+export { failedTransactionsCorrelationsSearchServiceProvider } from './failed_transactions_correlations_search_service';
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts
index 073bb122896ff..040aa5a7e424e 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts
@@ -5,8 +5,4 @@
* 2.0.
*/
-export {
- latencyCorrelationsSearchServiceProvider,
- LatencyCorrelationsSearchServiceProvider,
- LatencyCorrelationsSearchStrategy,
-} from './latency_correlations_search_service';
+export { latencyCorrelationsSearchServiceProvider } from './latency_correlations_search_service';
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts
index 4862f7dd1de1a..f170818d018d4 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts
@@ -8,15 +8,13 @@
import { range } from 'lodash';
import type { ElasticsearchClient } from 'src/core/server';
-import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server';
-import {
- IKibanaSearchRequest,
- IKibanaSearchResponse,
-} from '../../../../../../../src/plugins/data/common';
-
-import type { SearchStrategyServerParams } from '../../../../common/search_strategies/types';
import type {
- LatencyCorrelationsRequestParams,
+ RawResponseBase,
+ SearchStrategyClientParams,
+ SearchStrategyServerParams,
+} from '../../../../common/search_strategies/types';
+import type {
+ LatencyCorrelationsParams,
LatencyCorrelationsRawResponse,
} from '../../../../common/search_strategies/latency_correlations/types';
@@ -38,21 +36,16 @@ import type { SearchServiceProvider } from '../search_strategy_provider';
import { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state';
import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats';
-export type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider<
- LatencyCorrelationsRequestParams,
- LatencyCorrelationsRawResponse
->;
-
-export type LatencyCorrelationsSearchStrategy = ISearchStrategy<
- IKibanaSearchRequest,
- IKibanaSearchResponse
+type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider<
+ LatencyCorrelationsParams & SearchStrategyClientParams,
+ LatencyCorrelationsRawResponse & RawResponseBase
>;
export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearchServiceProvider =
(
esClient: ElasticsearchClient,
getApmIndices: () => Promise,
- searchServiceParams: LatencyCorrelationsRequestParams,
+ searchServiceParams: LatencyCorrelationsParams & SearchStrategyClientParams,
includeFrozen: boolean
) => {
const { addLogMessage, getLogMessages } = searchServiceLogProvider();
@@ -61,7 +54,9 @@ export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearch
async function fetchCorrelations() {
let params:
- | (LatencyCorrelationsRequestParams & SearchStrategyServerParams)
+ | (LatencyCorrelationsParams &
+ SearchStrategyClientParams &
+ SearchStrategyServerParams)
| undefined;
try {
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts
index deb89ace47c5d..d3cee1c4ca596 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts
@@ -15,8 +15,8 @@ import { fetchFieldsStats } from './get_fields_stats';
const params = {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts
index c77b4df78f865..9d0441e513198 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts
@@ -14,8 +14,8 @@ describe('correlations', () => {
const query = getQueryWithParams({
params: {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
@@ -45,8 +45,8 @@ describe('correlations', () => {
index: 'apm-*',
serviceName: 'actualServiceName',
transactionName: 'actualTransactionName',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
environment: 'dev',
kuery: '',
includeFrozen: false,
@@ -93,8 +93,8 @@ describe('correlations', () => {
const query = getQueryWithParams({
params: {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts
index f00c89503f103..31a98b0a6bb18 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts
@@ -6,15 +6,10 @@
*/
import type { estypes } from '@elastic/elasticsearch';
-import { getOrElse } from 'fp-ts/lib/Either';
-import { pipe } from 'fp-ts/lib/pipeable';
-import * as t from 'io-ts';
-import { failure } from 'io-ts/lib/PathReporter';
import type {
FieldValuePair,
SearchStrategyParams,
} from '../../../../common/search_strategies/types';
-import { rangeRt } from '../../../routes/default_api_types';
import { getCorrelationsFilters } from './get_filters';
export const getTermsQuery = ({ fieldName, fieldValue }: FieldValuePair) => {
@@ -36,22 +31,14 @@ export const getQueryWithParams = ({ params, termFilters }: QueryParams) => {
transactionName,
} = params;
- // converts string based start/end to epochmillis
- const decodedRange = pipe(
- rangeRt.decode({ start, end }),
- getOrElse((errors) => {
- throw new Error(failure(errors).join('\n'));
- })
- );
-
const correlationFilters = getCorrelationsFilters({
environment,
kuery,
serviceName,
transactionType,
transactionName,
- start: decodedRange.start,
- end: decodedRange.end,
+ start,
+ end,
});
return {
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts
index fd5f52207d4c5..eb771e1e1aaf4 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts
@@ -16,6 +16,8 @@ describe('correlations', () => {
includeFrozen: true,
environment: ENVIRONMENT_ALL.value,
kuery: '',
+ start: 1577836800000,
+ end: 1609459200000,
});
expect(requestBase).toEqual({
index: 'apm-*',
@@ -29,6 +31,8 @@ describe('correlations', () => {
index: 'apm-*',
environment: ENVIRONMENT_ALL.value,
kuery: '',
+ start: 1577836800000,
+ end: 1609459200000,
});
expect(requestBase).toEqual({
index: 'apm-*',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts
index fc2dacce61a73..40fcc17444492 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts
@@ -18,8 +18,8 @@ import {
const params = {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts
index 6e0521ac1a008..bae42666e6db0 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts
@@ -20,8 +20,8 @@ import {
const params = {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts
index 9ffbf6b2ce18d..ab7a0b4e02072 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts
@@ -20,8 +20,8 @@ import {
const params = {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts
index daf6b368c78b1..9c704ef7b489a 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts
@@ -17,8 +17,8 @@ import {
const params = {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts
index 7ecb1d2d8a333..7cc6106f671a7 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts
@@ -17,8 +17,8 @@ import {
const params = {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts
index ffc86c7ef6c32..41a2fa9a5039e 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts
@@ -17,8 +17,8 @@ import {
const params = {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts
index 790919d193028..439bb9e4b9cd6 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts
@@ -17,7 +17,11 @@ import type { SearchStrategyParams } from '../../../../common/search_strategies/
import { getQueryWithParams } from './get_query_with_params';
import { getRequestBase } from './get_request_base';
-const getHistogramRangeSteps = (min: number, max: number, steps: number) => {
+export const getHistogramRangeSteps = (
+ min: number,
+ max: number,
+ steps: number
+) => {
// A d3 based scale function as a helper to get equally distributed bins on a log scale.
// We round the final values because the ES range agg we use won't accept numbers with decimals for `transaction.duration.us`.
const logFn = scaleLog().domain([min, max]).range([1, steps]);
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts
index 375e32b1472c6..00e8c26497eb2 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts
@@ -17,8 +17,8 @@ import { fetchTransactionDurationHistograms } from './query_histograms_generator
const params = {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts
index ce86ffd9654e6..57e3e6cadb9bc 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts
@@ -17,8 +17,8 @@ import {
const params = {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts
index e210eb7d41e78..7d67e80ae3398 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts
@@ -17,8 +17,8 @@ import {
const params = {
index: 'apm-*',
- start: '2020',
- end: '2021',
+ start: 1577836800000,
+ end: 1609459200000,
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts
index 8a9d04df32036..034bd2a60ad19 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts
@@ -13,7 +13,7 @@ import { IKibanaSearchRequest } from '../../../../../../src/plugins/data/common'
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
import type { LatencyCorrelationsParams } from '../../../common/search_strategies/latency_correlations/types';
-import type { SearchStrategyClientParams } from '../../../common/search_strategies/types';
+import type { RawSearchStrategyClientParams } from '../../../common/search_strategies/types';
import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices';
@@ -112,7 +112,7 @@ describe('APM Correlations search strategy', () => {
let mockDeps: SearchStrategyDependencies;
let params: Required<
IKibanaSearchRequest<
- LatencyCorrelationsParams & SearchStrategyClientParams
+ LatencyCorrelationsParams & RawSearchStrategyClientParams
>
>['params'];
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts
index cec10294460b0..8035e9e4d97ca 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts
@@ -7,6 +7,10 @@
import uuid from 'uuid';
import { of } from 'rxjs';
+import { getOrElse } from 'fp-ts/lib/Either';
+import { pipe } from 'fp-ts/lib/pipeable';
+import * as t from 'io-ts';
+import { failure } from 'io-ts/lib/PathReporter';
import type { ElasticsearchClient } from 'src/core/server';
@@ -16,18 +20,21 @@ import {
IKibanaSearchResponse,
} from '../../../../../../src/plugins/data/common';
-import type { SearchStrategyClientParams } from '../../../common/search_strategies/types';
-import type { RawResponseBase } from '../../../common/search_strategies/types';
-import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices';
-
import type {
- LatencyCorrelationsSearchServiceProvider,
- LatencyCorrelationsSearchStrategy,
-} from './latency_correlations';
+ RawResponseBase,
+ RawSearchStrategyClientParams,
+ SearchStrategyClientParams,
+} from '../../../common/search_strategies/types';
+import type {
+ LatencyCorrelationsParams,
+ LatencyCorrelationsRawResponse,
+} from '../../../common/search_strategies/latency_correlations/types';
import type {
- FailedTransactionsCorrelationsSearchServiceProvider,
- FailedTransactionsCorrelationsSearchStrategy,
-} from './failed_transactions_correlations';
+ FailedTransactionsCorrelationsParams,
+ FailedTransactionsCorrelationsRawResponse,
+} from '../../../common/search_strategies/failed_transactions_correlations/types';
+import { rangeRt } from '../../routes/default_api_types';
+import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices';
interface SearchServiceState {
cancel: () => void;
@@ -56,35 +63,50 @@ export type SearchServiceProvider<
// Failed Transactions Correlations function overload
export function searchStrategyProvider(
- searchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider,
+ searchServiceProvider: SearchServiceProvider<
+ FailedTransactionsCorrelationsParams & SearchStrategyClientParams,
+ FailedTransactionsCorrelationsRawResponse & RawResponseBase
+ >,
getApmIndices: () => Promise,
includeFrozen: boolean
-): FailedTransactionsCorrelationsSearchStrategy;
+): ISearchStrategy<
+ IKibanaSearchRequest<
+ FailedTransactionsCorrelationsParams & RawSearchStrategyClientParams
+ >,
+ IKibanaSearchResponse<
+ FailedTransactionsCorrelationsRawResponse & RawResponseBase
+ >
+>;
// Latency Correlations function overload
export function searchStrategyProvider(
- searchServiceProvider: LatencyCorrelationsSearchServiceProvider,
+ searchServiceProvider: SearchServiceProvider<
+ LatencyCorrelationsParams & SearchStrategyClientParams,
+ LatencyCorrelationsRawResponse & RawResponseBase
+ >,
getApmIndices: () => Promise,
includeFrozen: boolean
-): LatencyCorrelationsSearchStrategy;
+): ISearchStrategy<
+ IKibanaSearchRequest<
+ LatencyCorrelationsParams & RawSearchStrategyClientParams
+ >,
+ IKibanaSearchResponse
+>;
-export function searchStrategyProvider<
- TSearchStrategyClientParams extends SearchStrategyClientParams,
- TRawResponse extends RawResponseBase
->(
+export function searchStrategyProvider(
searchServiceProvider: SearchServiceProvider<
- TSearchStrategyClientParams,
- TRawResponse
+ TRequestParams & SearchStrategyClientParams,
+ TResponseParams & RawResponseBase
>,
getApmIndices: () => Promise,
includeFrozen: boolean
): ISearchStrategy<
- IKibanaSearchRequest,
- IKibanaSearchResponse
+ IKibanaSearchRequest,
+ IKibanaSearchResponse
> {
const searchServiceMap = new Map<
string,
- GetSearchServiceState
+ GetSearchServiceState
>();
return {
@@ -93,9 +115,21 @@ export function searchStrategyProvider<
throw new Error('Invalid request parameters.');
}
+ const { start: startString, end: endString } = request.params;
+
+ // converts string based start/end to epochmillis
+ const decodedRange = pipe(
+ rangeRt.decode({ start: startString, end: endString }),
+ getOrElse((errors) => {
+ throw new Error(failure(errors).join('\n'));
+ })
+ );
+
// The function to fetch the current state of the search service.
// This will be either an existing service for a follow up fetch or a new one for new requests.
- let getSearchServiceState: GetSearchServiceState;
+ let getSearchServiceState: GetSearchServiceState<
+ TResponseParams & RawResponseBase
+ >;
// If the request includes an ID, we require that the search service already exists
// otherwise we throw an error. The client should never poll a service that's been cancelled or finished.
@@ -111,10 +145,30 @@ export function searchStrategyProvider<
getSearchServiceState = existingGetSearchServiceState;
} else {
+ const {
+ start,
+ end,
+ environment,
+ kuery,
+ serviceName,
+ transactionName,
+ transactionType,
+ ...requestParams
+ } = request.params;
+
getSearchServiceState = searchServiceProvider(
deps.esClient.asCurrentUser,
getApmIndices,
- request.params as TSearchStrategyClientParams,
+ {
+ environment,
+ kuery,
+ serviceName,
+ transactionName,
+ transactionType,
+ start: decodedRange.start,
+ end: decodedRange.end,
+ ...(requestParams as unknown as TRequestParams),
+ },
includeFrozen
);
}
diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
index 472e46fecfa10..3fa6152d953f3 100644
--- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
+++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
@@ -17,6 +17,7 @@ import { environmentsRouteRepository } from './environments';
import { errorsRouteRepository } from './errors';
import { apmFleetRouteRepository } from './fleet';
import { indexPatternRouteRepository } from './index_pattern';
+import { latencyDistributionRouteRepository } from './latency_distribution';
import { metricsRouteRepository } from './metrics';
import { observabilityOverviewRouteRepository } from './observability_overview';
import { rumRouteRepository } from './rum_client';
@@ -41,6 +42,7 @@ const getTypedGlobalApmServerRouteRepository = () => {
.merge(indexPatternRouteRepository)
.merge(environmentsRouteRepository)
.merge(errorsRouteRepository)
+ .merge(latencyDistributionRouteRepository)
.merge(metricsRouteRepository)
.merge(observabilityOverviewRouteRepository)
.merge(rumRouteRepository)
diff --git a/x-pack/plugins/apm/server/routes/latency_distribution.ts b/x-pack/plugins/apm/server/routes/latency_distribution.ts
new file mode 100644
index 0000000000000..ea921a7f4838d
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/latency_distribution.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 * as t from 'io-ts';
+import { toNumberRt } from '@kbn/io-ts-utils';
+import { getOverallLatencyDistribution } from '../lib/latency/get_overall_latency_distribution';
+import { setupRequest } from '../lib/helpers/setup_request';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
+import { environmentRt, kueryRt, rangeRt } from './default_api_types';
+
+const latencyOverallDistributionRoute = createApmServerRoute({
+ endpoint: 'GET /internal/apm/latency/overall_distribution',
+ params: t.type({
+ query: t.intersection([
+ t.partial({
+ serviceName: t.string,
+ transactionName: t.string,
+ transactionType: t.string,
+ }),
+ environmentRt,
+ kueryRt,
+ rangeRt,
+ t.type({
+ percentileThreshold: toNumberRt,
+ }),
+ ]),
+ }),
+ options: { tags: ['access:apm'] },
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+
+ const {
+ environment,
+ kuery,
+ serviceName,
+ transactionType,
+ transactionName,
+ start,
+ end,
+ percentileThreshold,
+ } = resources.params.query;
+
+ return getOverallLatencyDistribution({
+ environment,
+ kuery,
+ serviceName,
+ transactionType,
+ transactionName,
+ start,
+ end,
+ percentileThreshold,
+ setup,
+ });
+ },
+});
+
+export const latencyDistributionRouteRepository =
+ createApmServerRouteRepository().add(latencyOverallDistributionRoute);
diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts
index 6e2025a7fa2ca..3388d5b4aa379 100644
--- a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts
+++ b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts
@@ -10,7 +10,7 @@ import expect from '@kbn/expect';
import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common';
import type { FailedTransactionsCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/failed_transactions_correlations/types';
-import type { SearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types';
+import type { RawSearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types';
import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -23,7 +23,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const getRequestBody = () => {
const request: IKibanaSearchRequest<
- FailedTransactionsCorrelationsParams & SearchStrategyClientParams
+ FailedTransactionsCorrelationsParams & RawSearchStrategyClientParams
> = {
params: {
environment: 'ENVIRONMENT_ALL',
diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency.ts b/x-pack/test/apm_api_integration/tests/correlations/latency.ts
index 99aee770c625d..75a4edd447c70 100644
--- a/x-pack/test/apm_api_integration/tests/correlations/latency.ts
+++ b/x-pack/test/apm_api_integration/tests/correlations/latency.ts
@@ -10,7 +10,7 @@ import expect from '@kbn/expect';
import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common';
import type { LatencyCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/latency_correlations/types';
-import type { SearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types';
+import type { RawSearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types';
import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -22,16 +22,17 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('legacySupertestAsApmReadUser');
const getRequestBody = () => {
- const request: IKibanaSearchRequest = {
- params: {
- environment: 'ENVIRONMENT_ALL',
- start: '2020',
- end: '2021',
- kuery: '',
- percentileThreshold: 95,
- analyzeCorrelations: true,
- },
- };
+ const request: IKibanaSearchRequest =
+ {
+ params: {
+ environment: 'ENVIRONMENT_ALL',
+ start: '2020',
+ end: '2021',
+ kuery: '',
+ percentileThreshold: 95,
+ analyzeCorrelations: true,
+ },
+ };
return {
batch: [
diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts
index 09f4e2596ea46..f68a49658f2ee 100644
--- a/x-pack/test/apm_api_integration/tests/index.ts
+++ b/x-pack/test/apm_api_integration/tests/index.ts
@@ -175,6 +175,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte
loadTestFile(require.resolve('./transactions/error_rate'));
});
+ describe('transactions/latency_overall_distribution', function () {
+ loadTestFile(require.resolve('./transactions/latency_overall_distribution'));
+ });
+
describe('transactions/latency', function () {
loadTestFile(require.resolve('./transactions/latency'));
});
diff --git a/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts b/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts
new file mode 100644
index 0000000000000..c915ac8911e37
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 { FtrProviderContext } from '../../common/ftr_provider_context';
+import { registry } from '../../common/registry';
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const apmApiClient = getService('apmApiClient');
+
+ const endpoint = 'GET /internal/apm/latency/overall_distribution';
+
+ // This matches the parameters used for the other tab's search strategy approach in `../correlations/*`.
+ const getOptions = () => ({
+ params: {
+ query: {
+ environment: 'ENVIRONMENT_ALL',
+ start: '2020',
+ end: '2021',
+ kuery: '',
+ percentileThreshold: '95',
+ },
+ },
+ });
+
+ registry.when(
+ 'latency overall distribution without data',
+ { config: 'trial', archives: [] },
+ () => {
+ it('handles the empty state', async () => {
+ const response = await apmApiClient.readUser({
+ endpoint,
+ ...getOptions(),
+ });
+
+ expect(response.status).to.be(200);
+ expect(response.body?.percentileThresholdValue).to.be(undefined);
+ expect(response.body?.overallHistogram?.length).to.be(undefined);
+ });
+ }
+ );
+
+ registry.when(
+ 'latency overall distribution with data and default args',
+ // This uses the same archive used for the other tab's search strategy approach in `../correlations/*`.
+ { config: 'trial', archives: ['8.0.0'] },
+ () => {
+ it('returns percentileThresholdValue and overall histogram', async () => {
+ const response = await apmApiClient.readUser({
+ endpoint,
+ ...getOptions(),
+ });
+
+ expect(response.status).to.eql(200);
+ // This matches the values returned for the other tab's search strategy approach in `../correlations/*`.
+ expect(response.body?.percentileThresholdValue).to.be(1309695.875);
+ expect(response.body?.overallHistogram?.length).to.be(101);
+ });
+ }
+ );
+}
From ae659e5c79c4250a08a6aa328320c9a0f209e31f Mon Sep 17 00:00:00 2001
From: Brian Seeders
Date: Mon, 18 Oct 2021 21:08:56 -0400
Subject: [PATCH 03/18] skip flaky suite (#115488)
---
.../test/security_solution_endpoint_api_int/apis/metadata.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts
index 35fe0cdd6da25..2dcf36cc42ae2 100644
--- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts
+++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts
@@ -24,7 +24,8 @@ export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
- describe('test metadata api', () => {
+ // Failing: See https://github.com/elastic/kibana/issues/115488
+ describe.skip('test metadata api', () => {
// TODO add this after endpoint package changes are merged and in snapshot
// describe('with .metrics-endpoint.metadata_united_default index', () => {
// });
From 28db91718b5b277c9df16248cb8942a71814b695 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 00:28:59 -0400
Subject: [PATCH 04/18] [APM] APM-Fleet integration version check & upgrade
message (#115297) (#115484)
Co-authored-by: Oliver Gupte
---
x-pack/plugins/apm/common/fleet.ts | 2 +
.../components/app/Settings/schema/index.tsx | 4 +-
.../schema/migrated/card_footer_content.tsx | 47 +++++++++++++
.../migrated/successful_migration_card.tsx | 30 ++++++++
.../migrated/upgrade_available_card.tsx | 51 ++++++++++++++
.../app/Settings/schema/schema_overview.tsx | 68 +++++--------------
.../public/components/shared/Links/kibana.ts | 11 +++
.../get_apm_package_policy_definition.ts | 7 +-
x-pack/plugins/apm/server/routes/fleet.ts | 6 +-
9 files changed, 170 insertions(+), 56 deletions(-)
create mode 100644 x-pack/plugins/apm/public/components/app/Settings/schema/migrated/card_footer_content.tsx
create mode 100644 x-pack/plugins/apm/public/components/app/Settings/schema/migrated/successful_migration_card.tsx
create mode 100644 x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx
diff --git a/x-pack/plugins/apm/common/fleet.ts b/x-pack/plugins/apm/common/fleet.ts
index 618cd20d66159..97551cc16b4be 100644
--- a/x-pack/plugins/apm/common/fleet.ts
+++ b/x-pack/plugins/apm/common/fleet.ts
@@ -6,3 +6,5 @@
*/
export const POLICY_ELASTIC_AGENT_ON_CLOUD = 'policy-elastic-agent-on-cloud';
+
+export const SUPPORTED_APM_PACKAGE_VERSION = '7.16.0';
diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx
index ac32e22fa3ded..b13046d34be94 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx
@@ -54,7 +54,8 @@ export function Schema() {
const isLoading = status !== FETCH_STATUS.SUCCESS;
const cloudApmMigrationEnabled = !!data.cloud_apm_migration_enabled;
const hasCloudAgentPolicy = !!data.has_cloud_agent_policy;
- const hasCloudApmPackagePolicy = !!data.has_cloud_apm_package_policy;
+ const cloudApmPackagePolicy = data.cloud_apm_package_policy;
+ const hasCloudApmPackagePolicy = !!cloudApmPackagePolicy;
const hasRequiredRole = !!data.has_required_role;
function updateLocalStorage(newStatus: FETCH_STATUS) {
@@ -90,6 +91,7 @@ export function Schema() {
cloudApmMigrationEnabled={cloudApmMigrationEnabled}
hasCloudAgentPolicy={hasCloudAgentPolicy}
hasRequiredRole={hasRequiredRole}
+ cloudApmPackagePolicy={cloudApmPackagePolicy}
/>
{isSwitchActive && (
+
+ {i18n.translate(
+ 'xpack.apm.settings.schema.success.viewIntegrationInFleet.buttonText',
+ { defaultMessage: 'View the APM integration in Fleet' }
+ )}
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.apm.settings.schema.success.returnText.serviceInventoryLink',
+ { defaultMessage: 'Service inventory' }
+ )}
+
+ ),
+ }}
+ />
+
+
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/successful_migration_card.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/successful_migration_card.tsx
new file mode 100644
index 0000000000000..839479fbbf652
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/successful_migration_card.tsx
@@ -0,0 +1,30 @@
+/*
+ * 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 { EuiCard, EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { CardFooterContent } from './card_footer_content';
+
+export function SuccessfulMigrationCard() {
+ return (
+ }
+ title={i18n.translate('xpack.apm.settings.schema.success.title', {
+ defaultMessage: 'Elastic Agent successfully setup!',
+ })}
+ description={i18n.translate(
+ 'xpack.apm.settings.schema.success.description',
+ {
+ defaultMessage:
+ 'Your APM integration is now setup and ready to receive data from your currently instrumented agents. Feel free to review the policies applied to your integtration.',
+ }
+ )}
+ footer={}
+ />
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx
new file mode 100644
index 0000000000000..8c10236335961
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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 { EuiCard, EuiIcon, EuiLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React from 'react';
+import { useUpgradeApmPackagePolicyHref } from '../../../../shared/Links/kibana';
+import { CardFooterContent } from './card_footer_content';
+
+export function UpgradeAvailableCard({
+ apmPackagePolicyId,
+}: {
+ apmPackagePolicyId: string | undefined;
+}) {
+ const upgradeApmPackagePolicyHref =
+ useUpgradeApmPackagePolicyHref(apmPackagePolicyId);
+
+ return (
+ }
+ title={i18n.translate(
+ 'xpack.apm.settings.schema.upgradeAvailable.title',
+ {
+ defaultMessage: 'APM integration upgrade available!',
+ }
+ )}
+ description={
+
+ {i18n.translate(
+ 'xpack.apm.settings.schema.upgradeAvailable.upgradePackagePolicyLink',
+ { defaultMessage: 'Upgrade your APM integration' }
+ )}
+
+ ),
+ }}
+ />
+ }
+ footer={}
+ />
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx
index 0031c102e8ae5..cead6cd8a6fb4 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx
@@ -19,11 +19,14 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
-import { APMLink } from '../../../shared/Links/apm/APMLink';
+import semverLt from 'semver/functions/lt';
+import { SUPPORTED_APM_PACKAGE_VERSION } from '../../../../../common/fleet';
+import { PackagePolicy } from '../../../../../../fleet/common/types';
import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink';
-import { useFleetCloudAgentPolicyHref } from '../../../shared/Links/kibana';
import rocketLaunchGraphic from './blog-rocket-720x420.png';
import { MigrationInProgressPanel } from './migration_in_progress_panel';
+import { UpgradeAvailableCard } from './migrated/upgrade_available_card';
+import { SuccessfulMigrationCard } from './migrated/successful_migration_card';
interface Props {
onSwitch: () => void;
@@ -34,6 +37,7 @@ interface Props {
cloudApmMigrationEnabled: boolean;
hasCloudAgentPolicy: boolean;
hasRequiredRole: boolean;
+ cloudApmPackagePolicy: PackagePolicy | undefined;
}
export function SchemaOverview({
onSwitch,
@@ -44,10 +48,13 @@ export function SchemaOverview({
cloudApmMigrationEnabled,
hasCloudAgentPolicy,
hasRequiredRole,
+ cloudApmPackagePolicy,
}: Props) {
- const fleetCloudAgentPolicyHref = useFleetCloudAgentPolicyHref();
const isDisabled =
!cloudApmMigrationEnabled || !hasCloudAgentPolicy || !hasRequiredRole;
+ const packageVersion = cloudApmPackagePolicy?.package?.version;
+ const isUpgradeAvailable =
+ packageVersion && semverLt(packageVersion, SUPPORTED_APM_PACKAGE_VERSION);
if (isLoading) {
return (
@@ -76,54 +83,13 @@ export function SchemaOverview({
-
- }
- title={i18n.translate('xpack.apm.settings.schema.success.title', {
- defaultMessage: 'Elastic Agent successfully setup!',
- })}
- description={i18n.translate(
- 'xpack.apm.settings.schema.success.description',
- {
- defaultMessage:
- 'Your APM integration is now setup and ready to receive data from your currently instrumented agents. Feel free to review the policies applied to your integtration.',
- }
- )}
- footer={
-
-
- {i18n.translate(
- 'xpack.apm.settings.schema.success.viewIntegrationInFleet.buttonText',
- { defaultMessage: 'View the APM integration in Fleet' }
- )}
-
-
-
-
-
- {i18n.translate(
- 'xpack.apm.settings.schema.success.returnText.serviceInventoryLink',
- { defaultMessage: 'Service inventory' }
- )}
-
- ),
- }}
- />
-
-
-
- }
- />
+ {isUpgradeAvailable ? (
+
+ ) : (
+
+ )}
diff --git a/x-pack/plugins/apm/public/components/shared/Links/kibana.ts b/x-pack/plugins/apm/public/components/shared/Links/kibana.ts
index bfb7cf849f567..c0bdf3a98aa31 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/kibana.ts
+++ b/x-pack/plugins/apm/public/components/shared/Links/kibana.ts
@@ -26,3 +26,14 @@ export function useFleetCloudAgentPolicyHref() {
} = useApmPluginContext();
return basePath.prepend('/app/fleet#/policies/policy-elastic-agent-on-cloud');
}
+
+export function useUpgradeApmPackagePolicyHref(packagePolicyId = '') {
+ const {
+ core: {
+ http: { basePath },
+ },
+ } = useApmPluginContext();
+ return basePath.prepend(
+ `/app/fleet/policies/policy-elastic-agent-on-cloud/upgrade-package-policy/${packagePolicyId}?from=integrations-policy-list`
+ );
+}
diff --git a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts
index 64b071b67d2bd..98b6a6489c47b 100644
--- a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts
+++ b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts
@@ -5,7 +5,10 @@
* 2.0.
*/
-import { POLICY_ELASTIC_AGENT_ON_CLOUD } from '../../../common/fleet';
+import {
+ POLICY_ELASTIC_AGENT_ON_CLOUD,
+ SUPPORTED_APM_PACKAGE_VERSION,
+} from '../../../common/fleet';
import { APMPluginSetupDependencies } from '../../types';
import { APM_PACKAGE_NAME } from './get_cloud_apm_package_policy';
@@ -36,7 +39,7 @@ export function getApmPackagePolicyDefinition(
],
package: {
name: APM_PACKAGE_NAME,
- version: '0.4.0',
+ version: SUPPORTED_APM_PACKAGE_VERSION,
title: 'Elastic APM',
},
};
diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts
index 2884c08ceb9a1..e18aefcd6e0d8 100644
--- a/x-pack/plugins/apm/server/routes/fleet.ts
+++ b/x-pack/plugins/apm/server/routes/fleet.ts
@@ -92,7 +92,7 @@ const fleetAgentsRoute = createApmServerRoute({
});
const saveApmServerSchemaRoute = createApmServerRoute({
- endpoint: 'POST /internal/apm/fleet/apm_server_schema',
+ endpoint: 'POST /api/apm/fleet/apm_server_schema',
options: { tags: ['access:apm', 'access:apm_write'] },
params: t.type({
body: t.type({
@@ -143,11 +143,13 @@ const getMigrationCheckRoute = createApmServerRoute({
fleetPluginStart,
})
: undefined;
+ const apmPackagePolicy = getApmPackagePolicy(cloudAgentPolicy);
return {
has_cloud_agent_policy: !!cloudAgentPolicy,
- has_cloud_apm_package_policy: !!getApmPackagePolicy(cloudAgentPolicy),
+ has_cloud_apm_package_policy: !!apmPackagePolicy,
cloud_apm_migration_enabled: cloudApmMigrationEnabled,
has_required_role: hasRequiredRole,
+ cloud_apm_package_policy: apmPackagePolicy,
};
},
});
From ad9f36e61bb7d735f60dfe4a988c155baeb4427e Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 01:18:06 -0400
Subject: [PATCH 05/18] [Uptime] Fix unhandled promise rejection failure
(#114883) (#115491)
* Fix unhandled promise rejection failure.
* Mock monaco to avoid editor-related errors failing test.
* Update assertion.
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Justin Kambic
---
.../components/fleet_package/custom_fields.test.tsx | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx
index 26ee26cc8ed7f..62c6f5598adb4 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx
@@ -313,11 +313,11 @@ describe.skip('', () => {
// resolve errors
fireEvent.click(monitorType);
- waitFor(() => {
- expect(getByText('http')).toBeInTheDocument();
- expect(getByText('tcp')).toBeInTheDocument();
- expect(getByText('icmp')).toBeInTheDocument();
- expect(queryByText('browser')).not.toBeInTheDocument();
+ await waitFor(() => {
+ expect(getByText('HTTP')).toBeInTheDocument();
+ expect(getByText('TCP')).toBeInTheDocument();
+ expect(getByText('ICMP')).toBeInTheDocument();
+ expect(queryByText('Browser')).not.toBeInTheDocument();
});
});
});
From f9a08d4495a1b91b13b33ae2063cd4493deef859 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 02:01:45 -0400
Subject: [PATCH 06/18] [Security Solution] Skip flakey test Configures a new
connector.Cases connectors Configures a new connector (#115440) (#115480)
Co-authored-by: Kevin Logan <56395104+kevinlog@users.noreply.github.com>
---
.../cypress/integration/cases/connectors.spec.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts
index 287d86c6fba9e..69b623de0b43c 100644
--- a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts
@@ -20,7 +20,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { CASES_URL } from '../../urls/navigation';
-describe('Cases connectors', () => {
+// Skipping flakey test: https://github.com/elastic/kibana/issues/115438
+describe.skip('Cases connectors', () => {
const configureResult = {
connector: {
id: 'e271c3b8-f702-4fbc-98e0-db942b573bbd',
From 3e17aff5476fb3466a564e7921f8067af347c3b5 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 03:33:51 -0400
Subject: [PATCH 07/18] [APM] Add readme for @elastic/apm-generator (#115368)
(#115506)
Co-authored-by: Dario Gieselaar
---
packages/elastic-apm-generator/README.md | 93 ++++++++++++++++++++++++
1 file changed, 93 insertions(+)
diff --git a/packages/elastic-apm-generator/README.md b/packages/elastic-apm-generator/README.md
index e69de29bb2d1d..e43187a8155d3 100644
--- a/packages/elastic-apm-generator/README.md
+++ b/packages/elastic-apm-generator/README.md
@@ -0,0 +1,93 @@
+# @elastic/apm-generator
+
+`@elastic/apm-generator` is an experimental tool to generate synthetic APM data. It is intended to be used for development and testing of the Elastic APM app in Kibana.
+
+At a high-level, the module works by modeling APM events/metricsets with [a fluent API](https://en.wikipedia.org/wiki/Fluent_interface). The models can then be serialized and converted to Elasticsearch documents. In the future we might support APM Server as an output as well.
+
+## Usage
+
+This section assumes that you've installed Kibana's dependencies by running `yarn kbn bootstrap` in the repository's root folder.
+
+This library can currently be used in two ways:
+
+- Imported as a Node.js module, for instance to be used in Kibana's functional test suite.
+- With a command line interface, to index data based on some example scenarios.
+
+### Using the Node.js module
+
+#### Concepts
+
+- `Service`: a logical grouping for a monitored service. A `Service` object contains fields like `service.name`, `service.environment` and `agent.name`.
+- `Instance`: a single instance of a monitored service. E.g., the workload for a monitored service might be spread across multiple containers. An `Instance` object contains fields like `service.node.name` and `container.id`.
+- `Timerange`: an object that will return an array of timestamps based on an interval and a rate. These timestamps can be used to generate events/metricsets.
+- `Transaction`, `Span`, `APMError` and `Metricset`: events/metricsets that occur on an instance. For more background, see the [explanation of the APM data model](https://www.elastic.co/guide/en/apm/get-started/7.15/apm-data-model.html)
+
+
+#### Example
+
+```ts
+import { service, timerange, toElasticsearchOutput } from '@elastic/apm-generator';
+
+const instance = service('synth-go', 'production', 'go')
+ .instance('instance-a');
+
+const from = new Date('2021-01-01T12:00:00.000Z').getTime();
+const to = new Date('2021-01-01T12:00:00.000Z').getTime() - 1;
+
+const traceEvents = timerange(from, to)
+ .interval('1m')
+ .rate(10)
+ .flatMap(timestamp => instance.transaction('GET /api/product/list')
+ .timestamp(timestamp)
+ .duration(1000)
+ .success()
+ .children(
+ instance.span('GET apm-*/_search', 'db', 'elasticsearch')
+ .timestamp(timestamp + 50)
+ .duration(900)
+ .destination('elasticsearch')
+ .success()
+ ).serialize()
+ );
+
+const metricsets = timerange(from, to)
+ .interval('30s')
+ .rate(1)
+ .flatMap(timestamp => instance.appMetrics({
+ 'system.memory.actual.free': 800,
+ 'system.memory.total': 1000,
+ 'system.cpu.total.norm.pct': 0.6,
+ 'system.process.cpu.total.norm.pct': 0.7,
+ }).timestamp(timestamp)
+ .serialize()
+ );
+
+const esEvents = toElasticsearchOutput(traceEvents.concat(metricsets));
+```
+
+#### Generating metricsets
+
+`@elastic/apm-generator` can also automatically generate transaction metrics, span destination metrics and transaction breakdown metrics based on the generated trace events. If we expand on the previous example:
+
+```ts
+import { getTransactionMetrics, getSpanDestinationMetrics, getBreakdownMetrics } from '@elastic/apm-generator';
+
+const esEvents = toElasticsearchOutput([
+ ...traceEvents,
+ ...getTransactionMetrics(traceEvents),
+ ...getSpanDestinationMetrics(traceEvents),
+ ...getBreakdownMetrics(traceEvents)
+]);
+```
+
+### CLI
+
+Via the CLI, you can upload examples. The supported examples are listed in `src/lib/es.ts`. A `--target` option that specifies the Elasticsearch URL should be defined when running the `example` command. Here's an example:
+
+`$ node packages/elastic-apm-generator/src/scripts/es.js example simple-trace --target=http://admin:changeme@localhost:9200`
+
+The following options are supported:
+- `to`: the end of the time range, in ISO format. By default, the current time will be used.
+- `from`: the start of the time range, in ISO format. By default, `to` minus 15 minutes will be used.
+- `apm-server-version`: the version used in the index names bootstrapped by APM Server, e.g. `7.16.0`. __If these indices do not exist, the script will exit with an error. It will not bootstrap the indices itself.__
+
From 374398c53bc710f5b5e4d072122ecf0e80beccd3 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 04:36:06 -0400
Subject: [PATCH 08/18] [Security Solution] fix endpoint list agent status
logic (#115286) (#115498)
Co-authored-by: Joey F. Poon
---
.../server/endpoint/routes/metadata/handlers.ts | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts
index 027107bcf1a59..e98cdc4f11404 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts
@@ -48,6 +48,7 @@ import {
} from './support/query_strategies';
import { NotFoundError } from '../../errors';
import { EndpointHostUnEnrolledError } from '../../services/metadata';
+import { getAgentStatus } from '../../../../../fleet/common/services/agent_status';
export interface MetadataRequestContext {
esClient?: IScopedClusterClient;
@@ -522,10 +523,11 @@ async function queryUnitedIndex(
const agentPolicy = agentPoliciesMap[agent.policy_id!];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const endpointPolicy = endpointPoliciesMap[agent.policy_id!];
+ const fleetAgentStatus = getAgentStatus(agent as Agent);
+
return {
metadata,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- host_status: fleetAgentStatusToEndpointHostStatus(agent.last_checkin_status!),
+ host_status: fleetAgentStatusToEndpointHostStatus(fleetAgentStatus),
policy_info: {
agent: {
applied: {
From d92578780150fc0296e1b25a7e1e5b36d07c9464 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 04:46:57 -0400
Subject: [PATCH 09/18] [Security Solution][Endpoint]Activity Log API/UX
changes (#114905) (#115492)
* rename legacy actions/responses
fixes elastic/security-team/issues/1702
* use correct name for responses index
refs elastic/kibana/pull/113621
* extract helper method to utils
* append endpoint responses docs to activity log
* Show completed responses on activity log
fixes elastic/security-team/issues/1703
* remove width restriction on date picker
* add a simple test to verify endpoint responses
fixes elastic/security-team/issues/1702
* find unique action_ids from `.fleet-actions` and `.logs-endpoint.actions-default` indices
fixes elastic/security-team/issues/1702
* do not filter out endpoint only actions/responses that did not make it to Fleet
review comments
* use a constant to manage various doc types
review comments
* refactor `getActivityLog`
Simplify `getActivityLog` so it is easier to reason with.
review comments
* skip this for now
will mock this better in a new PR
* improve types
* display endpoint actions similar to fleet actions, but with success icon color
* Correctly do mocks for tests
* Include only errored endpoint actions, remove successful duplicates
fixes elastic/security-team/issues/1703
* Update tests to use non duplicate action_ids
review comments
fixes elastic/security-team/issues/1703
* show correct action title
review fixes
* statusCode constant
review change
* rename
review changes
* Update translations.ts
refs https://github.com/elastic/kibana/pull/114905/commits/74a8340b5eb2e31faba67a4fbe656f74fe52d0a2
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Ashokaditya
---
.../common/endpoint/constants.ts | 4 +-
.../common/endpoint/types/actions.ts | 33 ++-
.../activity_log_date_range_picker/index.tsx | 1 -
.../view/details/components/log_entry.tsx | 101 ++++++-
.../components/log_entry_timeline_icon.tsx | 21 +-
.../view/details/endpoints.stories.tsx | 14 +-
.../pages/endpoint_hosts/view/index.test.tsx | 95 +++++--
.../pages/endpoint_hosts/view/translations.ts | 36 +++
.../endpoint/routes/actions/audit_log.test.ts | 213 ++++++++++++--
.../endpoint/routes/actions/isolation.ts | 29 +-
.../server/endpoint/routes/actions/mocks.ts | 32 +++
.../server/endpoint/services/actions.ts | 146 ++++------
.../endpoint/utils/audit_log_helpers.ts | 266 ++++++++++++++++++
.../server/endpoint/utils/index.ts | 2 +
.../endpoint/utils/yes_no_data_stream.test.ts | 100 +++++++
.../endpoint/utils/yes_no_data_stream.ts | 59 ++++
16 files changed, 949 insertions(+), 203 deletions(-)
create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts
diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts
index 6e9123da2dd9b..178a2b68a4aab 100644
--- a/x-pack/plugins/security_solution/common/endpoint/constants.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts
@@ -10,7 +10,7 @@
export const ENDPOINT_ACTIONS_DS = '.logs-endpoint.actions';
export const ENDPOINT_ACTIONS_INDEX = `${ENDPOINT_ACTIONS_DS}-default`;
export const ENDPOINT_ACTION_RESPONSES_DS = '.logs-endpoint.action.responses';
-export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTIONS_DS}-default`;
+export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTION_RESPONSES_DS}-default`;
export const eventsIndexPattern = 'logs-endpoint.events.*';
export const alertsIndexPattern = 'logs-endpoint.alerts-*';
@@ -60,3 +60,5 @@ export const UNISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/unisolate`;
/** Endpoint Actions Log Routes */
export const ENDPOINT_ACTION_LOG_ROUTE = `/api/endpoint/action_log/{agent_id}`;
export const ACTION_STATUS_ROUTE = `/api/endpoint/action_status`;
+
+export const failedFleetActionErrorCode = '424';
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
index bc46ca2f5b451..fb29297eb5929 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
@@ -10,6 +10,13 @@ import { ActionStatusRequestSchema, HostIsolationRequestSchema } from '../schema
export type ISOLATION_ACTIONS = 'isolate' | 'unisolate';
+export const ActivityLogItemTypes = {
+ ACTION: 'action' as const,
+ RESPONSE: 'response' as const,
+ FLEET_ACTION: 'fleetAction' as const,
+ FLEET_RESPONSE: 'fleetResponse' as const,
+};
+
interface EcsError {
code?: string;
id?: string;
@@ -87,8 +94,24 @@ export interface EndpointActionResponse {
action_data: EndpointActionData;
}
+export interface EndpointActivityLogAction {
+ type: typeof ActivityLogItemTypes.ACTION;
+ item: {
+ id: string;
+ data: LogsEndpointAction;
+ };
+}
+
+export interface EndpointActivityLogActionResponse {
+ type: typeof ActivityLogItemTypes.RESPONSE;
+ item: {
+ id: string;
+ data: LogsEndpointActionResponse;
+ };
+}
+
export interface ActivityLogAction {
- type: 'action';
+ type: typeof ActivityLogItemTypes.FLEET_ACTION;
item: {
// document _id
id: string;
@@ -97,7 +120,7 @@ export interface ActivityLogAction {
};
}
export interface ActivityLogActionResponse {
- type: 'response';
+ type: typeof ActivityLogItemTypes.FLEET_RESPONSE;
item: {
// document id
id: string;
@@ -105,7 +128,11 @@ export interface ActivityLogActionResponse {
data: EndpointActionResponse;
};
}
-export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse;
+export type ActivityLogEntry =
+ | ActivityLogAction
+ | ActivityLogActionResponse
+ | EndpointActivityLogAction
+ | EndpointActivityLogActionResponse;
export interface ActivityLog {
page: number;
pageSize: number;
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx
index 05887d82cacad..a57fa8d8e4ce5 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx
@@ -32,7 +32,6 @@ interface Range {
const DatePickerWrapper = styled.div`
width: ${(props) => props.theme.eui.fractions.single.percentage};
- max-width: 350px;
`;
const StickyFlexItem = styled(EuiFlexItem)`
background: ${(props) => `${props.theme.eui.euiHeaderBackgroundColor}`};
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
index bbe0a6f3afcd1..79af2ecb354fd 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
@@ -9,24 +9,34 @@ import React, { memo, useMemo } from 'react';
import styled from 'styled-components';
import { EuiComment, EuiText, EuiAvatarProps, EuiCommentProps, IconType } from '@elastic/eui';
-import { Immutable, ActivityLogEntry } from '../../../../../../../common/endpoint/types';
+import {
+ Immutable,
+ ActivityLogEntry,
+ ActivityLogItemTypes,
+} from '../../../../../../../common/endpoint/types';
import { FormattedRelativePreferenceDate } from '../../../../../../common/components/formatted_date';
import { LogEntryTimelineIcon } from './log_entry_timeline_icon';
+import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme';
import * as i18 from '../../translations';
const useLogEntryUIProps = (
- logEntry: Immutable
+ logEntry: Immutable,
+ theme: ReturnType
): {
actionEventTitle: string;
+ avatarColor: EuiAvatarProps['color'];
+ avatarIconColor: EuiAvatarProps['iconColor'];
avatarSize: EuiAvatarProps['size'];
commentText: string;
commentType: EuiCommentProps['type'];
displayComment: boolean;
displayResponseEvent: boolean;
+ failedActionEventTitle: string;
iconType: IconType;
isResponseEvent: boolean;
isSuccessful: boolean;
+ isCompleted: boolean;
responseEventTitle: string;
username: string | React.ReactNode;
} => {
@@ -34,15 +44,19 @@ const useLogEntryUIProps = (
let iconType: IconType = 'dot';
let commentType: EuiCommentProps['type'] = 'update';
let commentText: string = '';
+ let avatarColor: EuiAvatarProps['color'] = theme.euiColorLightestShade;
+ let avatarIconColor: EuiAvatarProps['iconColor'];
let avatarSize: EuiAvatarProps['size'] = 's';
+ let failedActionEventTitle: string = '';
let isIsolateAction: boolean = false;
let isResponseEvent: boolean = false;
let isSuccessful: boolean = false;
+ let isCompleted: boolean = false;
let displayComment: boolean = false;
let displayResponseEvent: boolean = true;
let username: EuiCommentProps['username'] = '';
- if (logEntry.type === 'action') {
+ if (logEntry.type === ActivityLogItemTypes.FLEET_ACTION) {
avatarSize = 'm';
commentType = 'regular';
commentText = logEntry.item.data.data.comment?.trim() ?? '';
@@ -59,13 +73,51 @@ const useLogEntryUIProps = (
displayComment = true;
}
}
- } else if (logEntry.type === 'response') {
+ }
+ if (logEntry.type === ActivityLogItemTypes.ACTION) {
+ avatarSize = 'm';
+ commentType = 'regular';
+ commentText = logEntry.item.data.EndpointActions.data.comment?.trim() ?? '';
+ displayResponseEvent = false;
+ iconType = 'lockOpen';
+ username = logEntry.item.data.user.id;
+ avatarIconColor = theme.euiColorVis9_behindText;
+ failedActionEventTitle = i18.ACTIVITY_LOG.LogEntry.action.failedEndpointReleaseAction;
+ if (logEntry.item.data.EndpointActions.data) {
+ const data = logEntry.item.data.EndpointActions.data;
+ if (data.command === 'isolate') {
+ iconType = 'lock';
+ failedActionEventTitle = i18.ACTIVITY_LOG.LogEntry.action.failedEndpointIsolateAction;
+ }
+ if (commentText) {
+ displayComment = true;
+ }
+ }
+ } else if (logEntry.type === ActivityLogItemTypes.FLEET_RESPONSE) {
isResponseEvent = true;
if (logEntry.item.data.action_data.command === 'isolate') {
isIsolateAction = true;
}
if (!!logEntry.item.data.completed_at && !logEntry.item.data.error) {
isSuccessful = true;
+ } else {
+ avatarColor = theme.euiColorVis9_behindText;
+ }
+ } else if (logEntry.type === ActivityLogItemTypes.RESPONSE) {
+ iconType = 'check';
+ isResponseEvent = true;
+ if (logEntry.item.data.EndpointActions.data.command === 'isolate') {
+ isIsolateAction = true;
+ }
+ if (logEntry.item.data.EndpointActions.completed_at) {
+ isCompleted = true;
+ if (!logEntry.item.data.error) {
+ isSuccessful = true;
+ avatarColor = theme.euiColorVis0_behindText;
+ } else {
+ isSuccessful = false;
+ avatarColor = theme.euiColorVis9_behindText;
+ }
}
}
@@ -75,13 +127,23 @@ const useLogEntryUIProps = (
const getResponseEventTitle = () => {
if (isIsolateAction) {
- if (isSuccessful) {
+ if (isCompleted) {
+ if (isSuccessful) {
+ return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndSuccessful;
+ }
+ return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndUnsuccessful;
+ } else if (isSuccessful) {
return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful;
} else {
return i18.ACTIVITY_LOG.LogEntry.response.isolationFailed;
}
} else {
- if (isSuccessful) {
+ if (isCompleted) {
+ if (isSuccessful) {
+ return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndSuccessful;
+ }
+ return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndUnsuccessful;
+ } else if (isSuccessful) {
return i18.ACTIVITY_LOG.LogEntry.response.unisolationSuccessful;
} else {
return i18.ACTIVITY_LOG.LogEntry.response.unisolationFailed;
@@ -91,18 +153,22 @@ const useLogEntryUIProps = (
return {
actionEventTitle,
+ avatarColor,
+ avatarIconColor,
avatarSize,
commentText,
commentType,
displayComment,
displayResponseEvent,
+ failedActionEventTitle,
iconType,
isResponseEvent,
isSuccessful,
+ isCompleted,
responseEventTitle: getResponseEventTitle(),
username,
};
- }, [logEntry]);
+ }, [logEntry, theme]);
};
const StyledEuiComment = styled(EuiComment)`
@@ -126,28 +192,41 @@ const StyledEuiComment = styled(EuiComment)`
`;
export const LogEntry = memo(({ logEntry }: { logEntry: Immutable }) => {
+ const theme = useEuiTheme();
const {
actionEventTitle,
+ avatarColor,
+ avatarIconColor,
avatarSize,
commentText,
commentType,
displayComment,
displayResponseEvent,
+ failedActionEventTitle,
iconType,
isResponseEvent,
- isSuccessful,
responseEventTitle,
username,
- } = useLogEntryUIProps(logEntry);
+ } = useLogEntryUIProps(logEntry, theme);
return (
}
- event={{displayResponseEvent ? responseEventTitle : actionEventTitle}}
+ event={
+
+ {displayResponseEvent
+ ? responseEventTitle
+ : failedActionEventTitle
+ ? failedActionEventTitle
+ : actionEventTitle}
+
+ }
timelineIcon={
-
+
}
data-test-subj="timelineEntry"
>
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx
index 3ff311cd8a139..25e7c7d2c4a49 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx
@@ -7,32 +7,27 @@
import React, { memo } from 'react';
import { EuiAvatar, EuiAvatarProps } from '@elastic/eui';
-import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme';
export const LogEntryTimelineIcon = memo(
({
+ avatarColor,
+ avatarIconColor,
avatarSize,
- isResponseEvent,
- isSuccessful,
iconType,
+ isResponseEvent,
}: {
+ avatarColor: EuiAvatarProps['color'];
+ avatarIconColor?: EuiAvatarProps['iconColor'];
avatarSize: EuiAvatarProps['size'];
- isResponseEvent: boolean;
- isSuccessful: boolean;
iconType: EuiAvatarProps['iconType'];
+ isResponseEvent: boolean;
}) => {
- const euiTheme = useEuiTheme();
-
return (
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx
index 123a51e5a52bd..717368a1ff3a0 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx
@@ -8,7 +8,11 @@
import React, { ComponentType } from 'react';
import moment from 'moment';
-import { ActivityLog, Immutable } from '../../../../../../common/endpoint/types';
+import {
+ ActivityLog,
+ Immutable,
+ ActivityLogItemTypes,
+} from '../../../../../../common/endpoint/types';
import { EndpointDetailsFlyoutTabs } from './components/endpoint_details_tabs';
import { EndpointActivityLog } from './endpoint_activity_log';
import { EndpointDetailsFlyout } from '.';
@@ -26,7 +30,7 @@ export const dummyEndpointActivityLog = (
endDate: moment().toString(),
data: [
{
- type: 'action',
+ type: ActivityLogItemTypes.FLEET_ACTION,
item: {
id: '',
data: {
@@ -44,7 +48,7 @@ export const dummyEndpointActivityLog = (
},
},
{
- type: 'action',
+ type: ActivityLogItemTypes.FLEET_ACTION,
item: {
id: '',
data: {
@@ -63,7 +67,7 @@ export const dummyEndpointActivityLog = (
},
},
{
- type: 'action',
+ type: ActivityLogItemTypes.FLEET_ACTION,
item: {
id: '',
data: {
@@ -82,7 +86,7 @@ export const dummyEndpointActivityLog = (
},
},
{
- type: 'action',
+ type: ActivityLogItemTypes.FLEET_ACTION,
item: {
id: '',
data: {
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 b2c438659b771..727c2e8a35024 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
@@ -42,6 +42,7 @@ import {
import { getCurrentIsolationRequestState } from '../store/selectors';
import { licenseService } from '../../../../common/hooks/use_license';
import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator';
+import { EndpointActionGenerator } from '../../../../../common/endpoint/data_generators/endpoint_action_generator';
import {
APP_PATH,
MANAGEMENT_PATH,
@@ -807,7 +808,7 @@ describe('when on the endpoint list page', () => {
let renderResult: ReturnType;
const agentId = 'some_agent_id';
- let getMockData: () => ActivityLog;
+ let getMockData: (option?: { hasLogsEndpointActionResponses?: boolean }) => ActivityLog;
beforeEach(async () => {
window.IntersectionObserver = jest.fn(() => ({
root: null,
@@ -828,10 +829,15 @@ describe('when on the endpoint list page', () => {
});
const fleetActionGenerator = new FleetActionGenerator('seed');
- const responseData = fleetActionGenerator.generateResponse({
+ const endpointActionGenerator = new EndpointActionGenerator('seed');
+ const endpointResponseData = endpointActionGenerator.generateResponse({
+ agent: { id: agentId },
+ });
+ const fleetResponseData = fleetActionGenerator.generateResponse({
agent_id: agentId,
});
- const actionData = fleetActionGenerator.generate({
+
+ const fleetActionData = fleetActionGenerator.generate({
agents: [agentId],
data: {
comment: 'some comment',
@@ -844,35 +850,49 @@ describe('when on the endpoint list page', () => {
},
});
- getMockData = () => ({
- page: 1,
- pageSize: 50,
- startDate: 'now-1d',
- endDate: 'now',
- data: [
- {
- type: 'response',
- item: {
- id: 'some_id_0',
- data: responseData,
+ getMockData = (hasLogsEndpointActionResponses?: {
+ hasLogsEndpointActionResponses?: boolean;
+ }) => {
+ const response: ActivityLog = {
+ page: 1,
+ pageSize: 50,
+ startDate: 'now-1d',
+ endDate: 'now',
+ data: [
+ {
+ type: 'fleetResponse',
+ item: {
+ id: 'some_id_1',
+ data: fleetResponseData,
+ },
},
- },
- {
- type: 'action',
- item: {
- id: 'some_id_1',
- data: actionData,
+ {
+ type: 'fleetAction',
+ item: {
+ id: 'some_id_2',
+ data: fleetActionData,
+ },
},
- },
- {
- type: 'action',
+ {
+ type: 'fleetAction',
+ item: {
+ id: 'some_id_3',
+ data: isolatedActionData,
+ },
+ },
+ ],
+ };
+ if (hasLogsEndpointActionResponses) {
+ response.data.unshift({
+ type: 'response',
item: {
- id: 'some_id_3',
- data: isolatedActionData,
+ id: 'some_id_0',
+ data: endpointResponseData,
},
- },
- ],
- });
+ });
+ }
+ return response;
+ };
renderResult = render();
await reactTestingLibrary.act(async () => {
@@ -912,6 +932,25 @@ describe('when on the endpoint list page', () => {
expect(`${logEntries[1]} .euiCommentTimeline__icon--regular`).not.toBe(null);
});
+ it('should display log accurately with endpoint responses', async () => {
+ const activityLogTab = await renderResult.findByTestId('activity_log');
+ reactTestingLibrary.act(() => {
+ reactTestingLibrary.fireEvent.click(activityLogTab);
+ });
+ await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged');
+ reactTestingLibrary.act(() => {
+ dispatchEndpointDetailsActivityLogChanged(
+ 'success',
+ getMockData({ hasLogsEndpointActionResponses: true })
+ );
+ });
+ const logEntries = await renderResult.queryAllByTestId('timelineEntry');
+ expect(logEntries.length).toEqual(4);
+ expect(`${logEntries[0]} .euiCommentTimeline__icon--update`).not.toBe(null);
+ expect(`${logEntries[1]} .euiCommentTimeline__icon--update`).not.toBe(null);
+ expect(`${logEntries[2]} .euiCommentTimeline__icon--regular`).not.toBe(null);
+ });
+
it('should display empty state when API call has failed', async () => {
const activityLogTab = await renderResult.findByTestId('activity_log');
reactTestingLibrary.act(() => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
index c8a29eed3fda7..9cd55a70005ec 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
@@ -56,8 +56,44 @@ export const ACTIVITY_LOG = {
defaultMessage: 'submitted request: Release host',
}
),
+ failedEndpointReleaseAction: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.failedEndpointReleaseAction',
+ {
+ defaultMessage: 'failed to submit request: Release host',
+ }
+ ),
+ failedEndpointIsolateAction: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.failedEndpointIsolateAction',
+ {
+ defaultMessage: 'failed to submit request: Isolate host',
+ }
+ ),
},
response: {
+ isolationCompletedAndSuccessful: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationCompletedAndSuccessful',
+ {
+ defaultMessage: 'Host isolation request completed by Endpoint',
+ }
+ ),
+ isolationCompletedAndUnsuccessful: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationCompletedAndUnsuccessful',
+ {
+ defaultMessage: 'Host isolation request completed by Endpoint with errors',
+ }
+ ),
+ unisolationCompletedAndSuccessful: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationCompletedAndSuccessful',
+ {
+ defaultMessage: 'Release request completed by Endpoint',
+ }
+ ),
+ unisolationCompletedAndUnsuccessful: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationCompletedAndUnsuccessful',
+ {
+ defaultMessage: 'Release request completed by Endpoint with errors',
+ }
+ ),
isolationSuccessful: i18n.translate(
'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationSuccessful',
{
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts
index 4bd63c83169e5..5ce7962000788 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts
@@ -30,9 +30,15 @@ import {
} from '../../mocks';
import { registerActionAuditLogRoutes } from './audit_log';
import uuid from 'uuid';
-import { aMockAction, aMockResponse, MockAction, mockSearchResult, MockResponse } from './mocks';
+import { mockAuditLogSearchResult, Results } from './mocks';
import { SecuritySolutionRequestHandlerContext } from '../../../types';
-import { ActivityLog } from '../../../../common/endpoint/types';
+import {
+ ActivityLog,
+ EndpointAction,
+ EndpointActionResponse,
+} from '../../../../common/endpoint/types';
+import { FleetActionGenerator } from '../../../../common/endpoint/data_generators/fleet_action_generator';
+import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator';
describe('Action Log API', () => {
describe('schema', () => {
@@ -93,17 +99,30 @@ describe('Action Log API', () => {
});
describe('response', () => {
- const mockID = 'XYZABC-000';
- const actionID = 'some-known-actionid';
+ const mockAgentID = 'XYZABC-000';
let endpointAppContextService: EndpointAppContextService;
+ const fleetActionGenerator = new FleetActionGenerator('seed');
+ const endpointActionGenerator = new EndpointActionGenerator('seed');
// convenience for calling the route and handler for audit log
let getActivityLog: (
params: EndpointActionLogRequestParams,
query?: EndpointActionLogRequestQuery
) => Promise>;
- // convenience for injecting mock responses for actions index and responses
- let havingActionsAndResponses: (actions: MockAction[], responses: MockResponse[]) => void;
+
+ // convenience for injecting mock action requests and responses
+ // for .logs-endpoint and .fleet indices
+ let mockActions: ({
+ numActions,
+ hasFleetActions,
+ hasFleetResponses,
+ hasResponses,
+ }: {
+ numActions: number;
+ hasFleetActions?: boolean;
+ hasFleetResponses?: boolean;
+ hasResponses?: boolean;
+ }) => void;
let havingErrors: () => void;
@@ -149,12 +168,113 @@ describe('Action Log API', () => {
return mockResponse;
};
- havingActionsAndResponses = (actions: MockAction[], responses: MockResponse[]) => {
- esClientMock.asCurrentUser.search = jest.fn().mockImplementation((req) => {
- const items: any[] =
- req.index === '.fleet-actions' ? actions.splice(0, 50) : responses.splice(0, 1000);
+ // some arbitrary ids for needed actions
+ const getMockActionIds = (numAction: number): string[] => {
+ return [...Array(numAction).keys()].map(() => Math.random().toString(36).split('.')[1]);
+ };
+
+ // create as many actions as needed
+ const getEndpointActionsData = (actionIds: string[]) => {
+ const data = actionIds.map((actionId) =>
+ endpointActionGenerator.generate({
+ agent: { id: mockAgentID },
+ EndpointActions: {
+ action_id: actionId,
+ },
+ })
+ );
+ return data;
+ };
+ // create as many responses as needed
+ const getEndpointResponseData = (actionIds: string[]) => {
+ const data = actionIds.map((actionId) =>
+ endpointActionGenerator.generateResponse({
+ agent: { id: mockAgentID },
+ EndpointActions: {
+ action_id: actionId,
+ },
+ })
+ );
+ return data;
+ };
+ // create as many fleet actions as needed
+ const getFleetResponseData = (actionIds: string[]) => {
+ const data = actionIds.map((actionId) =>
+ fleetActionGenerator.generateResponse({
+ agent_id: mockAgentID,
+ action_id: actionId,
+ })
+ );
+ return data;
+ };
+ // create as many fleet responses as needed
+ const getFleetActionData = (actionIds: string[]) => {
+ const data = actionIds.map((actionId) =>
+ fleetActionGenerator.generate({
+ agents: [mockAgentID],
+ action_id: actionId,
+ data: {
+ comment: 'some comment',
+ },
+ })
+ );
+ return data;
+ };
+
+ // mock actions and responses results in a single response
+ mockActions = ({
+ numActions,
+ hasFleetActions = false,
+ hasFleetResponses = false,
+ hasResponses = false,
+ }: {
+ numActions: number;
+ hasFleetActions?: boolean;
+ hasFleetResponses?: boolean;
+ hasResponses?: boolean;
+ }) => {
+ esClientMock.asCurrentUser.search = jest.fn().mockImplementationOnce(() => {
+ let actions: Results[] = [];
+ let fleetActions: Results[] = [];
+ let responses: Results[] = [];
+ let fleetResponses: Results[] = [];
+
+ const actionIds = getMockActionIds(numActions);
+
+ actions = getEndpointActionsData(actionIds).map((e) => ({
+ _index: '.ds-.logs-endpoint.actions-default-2021.19.10-000001',
+ _source: e,
+ }));
+
+ if (hasFleetActions) {
+ fleetActions = getFleetActionData(actionIds).map((e) => ({
+ _index: '.fleet-actions-7',
+ _source: e,
+ }));
+ }
- return Promise.resolve(mockSearchResult(items.map((x) => x.build())));
+ if (hasFleetResponses) {
+ fleetResponses = getFleetResponseData(actionIds).map((e) => ({
+ _index: '.ds-.fleet-actions-results-2021.19.10-000001',
+ _source: e,
+ }));
+ }
+
+ if (hasResponses) {
+ responses = getEndpointResponseData(actionIds).map((e) => ({
+ _index: '.ds-.logs-endpoint.action.responses-default-2021.19.10-000001',
+ _source: e,
+ }));
+ }
+
+ const results = mockAuditLogSearchResult([
+ ...actions,
+ ...fleetActions,
+ ...responses,
+ ...fleetResponses,
+ ]);
+
+ return Promise.resolve(results);
});
};
@@ -172,45 +292,80 @@ describe('Action Log API', () => {
});
it('should return an empty array when nothing in audit log', async () => {
- havingActionsAndResponses([], []);
- const response = await getActivityLog({ agent_id: mockID });
+ mockActions({ numActions: 0 });
+
+ const response = await getActivityLog({ agent_id: mockAgentID });
expect(response.ok).toBeCalled();
expect((response.ok.mock.calls[0][0]?.body as ActivityLog).data).toHaveLength(0);
});
- it('should have actions and action responses', async () => {
- havingActionsAndResponses(
- [
- aMockAction().withAgent(mockID).withAction('isolate').withID(actionID),
- aMockAction().withAgent(mockID).withAction('unisolate'),
- ],
- [aMockResponse(actionID, mockID).forAction(actionID).forAgent(mockID)]
- );
- const response = await getActivityLog({ agent_id: mockID });
+ it('should return fleet actions, fleet responses and endpoint responses', async () => {
+ mockActions({
+ numActions: 2,
+ hasFleetActions: true,
+ hasFleetResponses: true,
+ hasResponses: true,
+ });
+
+ const response = await getActivityLog({ agent_id: mockAgentID });
+ const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog;
+ expect(response.ok).toBeCalled();
+ expect(responseBody.data).toHaveLength(6);
+
+ expect(
+ responseBody.data.filter((e) => (e.item.data as EndpointActionResponse).completed_at)
+ ).toHaveLength(2);
+ expect(
+ responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration)
+ ).toHaveLength(2);
+ });
+
+ it('should return only fleet actions and no responses', async () => {
+ mockActions({ numActions: 2, hasFleetActions: true });
+
+ const response = await getActivityLog({ agent_id: mockAgentID });
const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog;
+ expect(response.ok).toBeCalled();
+ expect(responseBody.data).toHaveLength(2);
+
+ expect(
+ responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration)
+ ).toHaveLength(2);
+ });
+
+ it('should only have fleet data', async () => {
+ mockActions({ numActions: 2, hasFleetActions: true, hasFleetResponses: true });
+ const response = await getActivityLog({ agent_id: mockAgentID });
+ const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog;
expect(response.ok).toBeCalled();
- expect(responseBody.data).toHaveLength(3);
- expect(responseBody.data.filter((e) => e.type === 'response')).toHaveLength(1);
- expect(responseBody.data.filter((e) => e.type === 'action')).toHaveLength(2);
+ expect(responseBody.data).toHaveLength(4);
+
+ expect(
+ responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration)
+ ).toHaveLength(2);
+ expect(
+ responseBody.data.filter((e) => (e.item.data as EndpointActionResponse).completed_at)
+ ).toHaveLength(2);
});
it('should throw errors when no results for some agentID', async () => {
havingErrors();
try {
- await getActivityLog({ agent_id: mockID });
+ await getActivityLog({ agent_id: mockAgentID });
} catch (error) {
- expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockID}`);
+ expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockAgentID}`);
}
});
it('should return date ranges if present in the query', async () => {
- havingActionsAndResponses([], []);
+ mockActions({ numActions: 0 });
+
const startDate = new Date(new Date().setDate(new Date().getDate() - 1)).toISOString();
const endDate = new Date().toISOString();
const response = await getActivityLog(
- { agent_id: mockID },
+ { agent_id: mockAgentID },
{
page: 1,
page_size: 50,
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
index e12299bedbb34..02f0cb4867646 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
@@ -17,6 +17,7 @@ import {
ENDPOINT_ACTION_RESPONSES_DS,
ISOLATE_HOST_ROUTE,
UNISOLATE_HOST_ROUTE,
+ failedFleetActionErrorCode,
} from '../../../../common/endpoint/constants';
import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common';
import {
@@ -33,6 +34,7 @@ import { getMetadataForEndpoints } from '../../services';
import { EndpointAppContext } from '../../types';
import { APP_ID } from '../../../../common/constants';
import { userCanIsolate } from '../../../../common/endpoint/actions';
+import { doLogsEndpointActionDsExists } from '../../utils';
/**
* Registers the Host-(un-)isolation routes
@@ -78,7 +80,7 @@ const createFailedActionResponseEntry = async ({
body: {
...doc,
error: {
- code: '424',
+ code: failedFleetActionErrorCode,
message: 'Failed to deliver action request to fleet',
},
},
@@ -88,31 +90,6 @@ const createFailedActionResponseEntry = async ({
}
};
-const doLogsEndpointActionDsExists = async ({
- context,
- logger,
- dataStreamName,
-}: {
- context: SecuritySolutionRequestHandlerContext;
- logger: Logger;
- dataStreamName: string;
-}): Promise => {
- try {
- const esClient = context.core.elasticsearch.client.asInternalUser;
- const doesIndexTemplateExist = await esClient.indices.existsIndexTemplate({
- name: dataStreamName,
- });
- return doesIndexTemplateExist.statusCode === 404 ? false : true;
- } catch (error) {
- const errorType = error?.type ?? '';
- if (errorType !== 'resource_not_found_exception') {
- logger.error(error);
- throw error;
- }
- return false;
- }
-};
-
export const isolationRequestHandler = function (
endpointContext: EndpointAppContext,
isolate: boolean
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts
index 34f7d140a78de..b50d80a9bae71 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts
@@ -13,11 +13,43 @@ import { ApiResponse } from '@elastic/elasticsearch';
import moment from 'moment';
import uuid from 'uuid';
import {
+ LogsEndpointAction,
+ LogsEndpointActionResponse,
EndpointAction,
EndpointActionResponse,
ISOLATION_ACTIONS,
} from '../../../../common/endpoint/types';
+export interface Results {
+ _index: string;
+ _source:
+ | LogsEndpointAction
+ | LogsEndpointActionResponse
+ | EndpointAction
+ | EndpointActionResponse;
+}
+export const mockAuditLogSearchResult = (results?: Results[]) => {
+ const response = {
+ body: {
+ hits: {
+ total: { value: results?.length ?? 0, relation: 'eq' },
+ hits:
+ results?.map((a: Results) => ({
+ _index: a._index,
+ _id: Math.random().toString(36).split('.')[1],
+ _score: 0.0,
+ _source: a._source,
+ })) ?? [],
+ },
+ },
+ statusCode: 200,
+ headers: {},
+ warnings: [],
+ meta: {} as any,
+ };
+ return response;
+};
+
export const mockSearchResult = (results: any = []): ApiResponse => {
return {
body: {
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts
index 711d78ba51b59..d59ecb674196c 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts
@@ -6,15 +6,28 @@
*/
import { ElasticsearchClient, Logger } from 'kibana/server';
+import { SearchHit, SearchResponse } from '@elastic/elasticsearch/api/types';
+import { ApiResponse } from '@elastic/elasticsearch';
import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common';
import { SecuritySolutionRequestHandlerContext } from '../../types';
import {
ActivityLog,
+ ActivityLogEntry,
EndpointAction,
+ LogsEndpointAction,
EndpointActionResponse,
EndpointPendingActions,
+ LogsEndpointActionResponse,
} from '../../../common/endpoint/types';
-import { catchAndWrapError } from '../utils';
+import {
+ catchAndWrapError,
+ categorizeActionResults,
+ categorizeResponseResults,
+ getActionRequestsResult,
+ getActionResponsesResult,
+ getTimeSortedData,
+ getUniqueLogData,
+} from '../utils';
import { EndpointMetadataService } from './metadata';
const PENDING_ACTION_RESPONSE_MAX_LAPSED_TIME = 300000; // 300k ms === 5 minutes
@@ -38,9 +51,9 @@ export const getAuditLogResponse = async ({
}): Promise => {
const size = Math.floor(pageSize / 2);
const from = page <= 1 ? 0 : page * size - size + 1;
- const esClient = context.core.elasticsearch.client.asCurrentUser;
+
const data = await getActivityLog({
- esClient,
+ context,
from,
size,
startDate,
@@ -59,7 +72,7 @@ export const getAuditLogResponse = async ({
};
const getActivityLog = async ({
- esClient,
+ context,
size,
from,
startDate,
@@ -67,83 +80,39 @@ const getActivityLog = async ({
elasticAgentId,
logger,
}: {
- esClient: ElasticsearchClient;
+ context: SecuritySolutionRequestHandlerContext;
elasticAgentId: string;
size: number;
from: number;
startDate: string;
endDate: string;
logger: Logger;
-}) => {
- const options = {
- headers: {
- 'X-elastic-product-origin': 'fleet',
- },
- ignore: [404],
- };
-
- let actionsResult;
- let responsesResult;
- const dateFilters = [
- { range: { '@timestamp': { gte: startDate } } },
- { range: { '@timestamp': { lte: endDate } } },
- ];
+}): Promise => {
+ let actionsResult: ApiResponse, unknown>;
+ let responsesResult: ApiResponse, unknown>;
try {
// fetch actions with matching agent_id
- const baseActionFilters = [
- { term: { agents: elasticAgentId } },
- { term: { input_type: 'endpoint' } },
- { term: { type: 'INPUT_ACTION' } },
- ];
- const actionsFilters = [...baseActionFilters, ...dateFilters];
- actionsResult = await esClient.search(
- {
- index: AGENT_ACTIONS_INDEX,
- size,
- from,
- body: {
- query: {
- bool: {
- // @ts-ignore
- filter: actionsFilters,
- },
- },
- sort: [
- {
- '@timestamp': {
- order: 'desc',
- },
- },
- ],
- },
- },
- options
- );
- const actionIds = actionsResult?.body?.hits?.hits?.map(
- (e) => (e._source as EndpointAction).action_id
- );
+ const { actionIds, actionRequests } = await getActionRequestsResult({
+ context,
+ logger,
+ elasticAgentId,
+ startDate,
+ endDate,
+ size,
+ from,
+ });
+ actionsResult = actionRequests;
- // fetch responses with matching `action_id`s
- const baseResponsesFilter = [
- { term: { agent_id: elasticAgentId } },
- { terms: { action_id: actionIds } },
- ];
- const responsesFilters = [...baseResponsesFilter, ...dateFilters];
- responsesResult = await esClient.search(
- {
- index: AGENT_ACTIONS_RESULTS_INDEX,
- size: 1000,
- body: {
- query: {
- bool: {
- filter: responsesFilters,
- },
- },
- },
- },
- options
- );
+ // fetch responses with matching unique set of `action_id`s
+ responsesResult = await getActionResponsesResult({
+ actionIds: [...new Set(actionIds)], // de-dupe `action_id`s
+ context,
+ logger,
+ elasticAgentId,
+ startDate,
+ endDate,
+ });
} catch (error) {
logger.error(error);
throw error;
@@ -153,21 +122,26 @@ const getActivityLog = async ({
throw new Error(`Error fetching actions log for agent_id ${elasticAgentId}`);
}
- const responses = responsesResult?.body?.hits?.hits?.length
- ? responsesResult?.body?.hits?.hits?.map((e) => ({
- type: 'response',
- item: { id: e._id, data: e._source },
- }))
- : [];
- const actions = actionsResult?.body?.hits?.hits?.length
- ? actionsResult?.body?.hits?.hits?.map((e) => ({
- type: 'action',
- item: { id: e._id, data: e._source },
- }))
- : [];
- const sortedData = ([...responses, ...actions] as ActivityLog['data']).sort((a, b) =>
- new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1
- );
+ // label record as `action`, `fleetAction`
+ const responses = categorizeResponseResults({
+ results: responsesResult?.body?.hits?.hits as Array<
+ SearchHit
+ >,
+ });
+
+ // label record as `response`, `fleetResponse`
+ const actions = categorizeActionResults({
+ results: actionsResult?.body?.hits?.hits as Array<
+ SearchHit
+ >,
+ });
+
+ // filter out the duplicate endpoint actions that also have fleetActions
+ // include endpoint actions that have no fleet actions
+ const uniqueLogData = getUniqueLogData([...responses, ...actions]);
+
+ // sort by @timestamp in desc order, newest first
+ const sortedData = getTimeSortedData(uniqueLogData);
return sortedData;
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts b/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts
new file mode 100644
index 0000000000000..f75b265bf24d7
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts
@@ -0,0 +1,266 @@
+/*
+ * 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 { Logger } from 'kibana/server';
+import { SearchRequest } from 'src/plugins/data/public';
+import { SearchHit, SearchResponse } from '@elastic/elasticsearch/api/types';
+import { ApiResponse } from '@elastic/elasticsearch';
+import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common';
+import {
+ ENDPOINT_ACTIONS_INDEX,
+ ENDPOINT_ACTION_RESPONSES_INDEX,
+ failedFleetActionErrorCode,
+} from '../../../common/endpoint/constants';
+import { SecuritySolutionRequestHandlerContext } from '../../types';
+import {
+ ActivityLog,
+ ActivityLogAction,
+ EndpointActivityLogAction,
+ ActivityLogActionResponse,
+ EndpointActivityLogActionResponse,
+ ActivityLogItemTypes,
+ EndpointAction,
+ LogsEndpointAction,
+ EndpointActionResponse,
+ LogsEndpointActionResponse,
+ ActivityLogEntry,
+} from '../../../common/endpoint/types';
+import { doesLogsEndpointActionsIndexExist } from '../utils';
+
+const actionsIndices = [AGENT_ACTIONS_INDEX, ENDPOINT_ACTIONS_INDEX];
+const responseIndices = [AGENT_ACTIONS_RESULTS_INDEX, ENDPOINT_ACTION_RESPONSES_INDEX];
+export const logsEndpointActionsRegex = new RegExp(`(^\.ds-\.logs-endpoint\.actions-default-).+`);
+export const logsEndpointResponsesRegex = new RegExp(
+ `(^\.ds-\.logs-endpoint\.action\.responses-default-).+`
+);
+const queryOptions = {
+ headers: {
+ 'X-elastic-product-origin': 'fleet',
+ },
+ ignore: [404],
+};
+
+const getDateFilters = ({ startDate, endDate }: { startDate: string; endDate: string }) => {
+ return [
+ { range: { '@timestamp': { gte: startDate } } },
+ { range: { '@timestamp': { lte: endDate } } },
+ ];
+};
+
+export const getUniqueLogData = (activityLogEntries: ActivityLogEntry[]): ActivityLogEntry[] => {
+ // find the error responses for actions that didn't make it to fleet index
+ const onlyResponsesForFleetErrors = activityLogEntries
+ .filter(
+ (e) =>
+ e.type === ActivityLogItemTypes.RESPONSE &&
+ e.item.data.error?.code === failedFleetActionErrorCode
+ )
+ .map(
+ (e: ActivityLogEntry) => (e.item.data as LogsEndpointActionResponse).EndpointActions.action_id
+ );
+
+ // all actions and responses minus endpoint actions.
+ const nonEndpointActionsDocs = activityLogEntries.filter(
+ (e) => e.type !== ActivityLogItemTypes.ACTION
+ );
+
+ // only endpoint actions that match the error responses
+ const onlyEndpointActionsDocWithoutFleetActions = activityLogEntries
+ .filter((e) => e.type === ActivityLogItemTypes.ACTION)
+ .filter((e: ActivityLogEntry) =>
+ onlyResponsesForFleetErrors.includes(
+ (e.item.data as LogsEndpointAction).EndpointActions.action_id
+ )
+ );
+
+ // join the error actions and the rest
+ return [...nonEndpointActionsDocs, ...onlyEndpointActionsDocWithoutFleetActions];
+};
+
+export const categorizeResponseResults = ({
+ results,
+}: {
+ results: Array>;
+}): Array => {
+ return results?.length
+ ? results?.map((e) => {
+ const isResponseDoc: boolean = matchesIndexPattern({
+ regexPattern: logsEndpointResponsesRegex,
+ index: e._index,
+ });
+ return isResponseDoc
+ ? {
+ type: ActivityLogItemTypes.RESPONSE,
+ item: { id: e._id, data: e._source as LogsEndpointActionResponse },
+ }
+ : {
+ type: ActivityLogItemTypes.FLEET_RESPONSE,
+ item: { id: e._id, data: e._source as EndpointActionResponse },
+ };
+ })
+ : [];
+};
+
+export const categorizeActionResults = ({
+ results,
+}: {
+ results: Array>;
+}): Array => {
+ return results?.length
+ ? results?.map((e) => {
+ const isActionDoc: boolean = matchesIndexPattern({
+ regexPattern: logsEndpointActionsRegex,
+ index: e._index,
+ });
+ return isActionDoc
+ ? {
+ type: ActivityLogItemTypes.ACTION,
+ item: { id: e._id, data: e._source as LogsEndpointAction },
+ }
+ : {
+ type: ActivityLogItemTypes.FLEET_ACTION,
+ item: { id: e._id, data: e._source as EndpointAction },
+ };
+ })
+ : [];
+};
+
+export const getTimeSortedData = (data: ActivityLog['data']): ActivityLog['data'] => {
+ return data.sort((a, b) =>
+ new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1
+ );
+};
+
+export const getActionRequestsResult = async ({
+ context,
+ logger,
+ elasticAgentId,
+ startDate,
+ endDate,
+ size,
+ from,
+}: {
+ context: SecuritySolutionRequestHandlerContext;
+ logger: Logger;
+ elasticAgentId: string;
+ startDate: string;
+ endDate: string;
+ size: number;
+ from: number;
+}): Promise<{
+ actionIds: string[];
+ actionRequests: ApiResponse, unknown>;
+}> => {
+ const dateFilters = getDateFilters({ startDate, endDate });
+ const baseActionFilters = [
+ { term: { agents: elasticAgentId } },
+ { term: { input_type: 'endpoint' } },
+ { term: { type: 'INPUT_ACTION' } },
+ ];
+ const actionsFilters = [...baseActionFilters, ...dateFilters];
+
+ const hasLogsEndpointActionsIndex = await doesLogsEndpointActionsIndexExist({
+ context,
+ logger,
+ indexName: ENDPOINT_ACTIONS_INDEX,
+ });
+
+ const actionsSearchQuery: SearchRequest = {
+ index: hasLogsEndpointActionsIndex ? actionsIndices : AGENT_ACTIONS_INDEX,
+ size,
+ from,
+ body: {
+ query: {
+ bool: {
+ filter: actionsFilters,
+ },
+ },
+ sort: [
+ {
+ '@timestamp': {
+ order: 'desc',
+ },
+ },
+ ],
+ },
+ };
+
+ let actionRequests: ApiResponse, unknown>;
+ try {
+ const esClient = context.core.elasticsearch.client.asCurrentUser;
+ actionRequests = await esClient.search(actionsSearchQuery, queryOptions);
+ const actionIds = actionRequests?.body?.hits?.hits?.map((e) => {
+ return logsEndpointActionsRegex.test(e._index)
+ ? (e._source as LogsEndpointAction).EndpointActions.action_id
+ : (e._source as EndpointAction).action_id;
+ });
+
+ return { actionIds, actionRequests };
+ } catch (error) {
+ logger.error(error);
+ throw error;
+ }
+};
+
+export const getActionResponsesResult = async ({
+ context,
+ logger,
+ elasticAgentId,
+ actionIds,
+ startDate,
+ endDate,
+}: {
+ context: SecuritySolutionRequestHandlerContext;
+ logger: Logger;
+ elasticAgentId: string;
+ actionIds: string[];
+ startDate: string;
+ endDate: string;
+}): Promise, unknown>> => {
+ const dateFilters = getDateFilters({ startDate, endDate });
+ const baseResponsesFilter = [
+ { term: { agent_id: elasticAgentId } },
+ { terms: { action_id: actionIds } },
+ ];
+ const responsesFilters = [...baseResponsesFilter, ...dateFilters];
+
+ const hasLogsEndpointActionResponsesIndex = await doesLogsEndpointActionsIndexExist({
+ context,
+ logger,
+ indexName: ENDPOINT_ACTION_RESPONSES_INDEX,
+ });
+
+ const responsesSearchQuery: SearchRequest = {
+ index: hasLogsEndpointActionResponsesIndex ? responseIndices : AGENT_ACTIONS_RESULTS_INDEX,
+ size: 1000,
+ body: {
+ query: {
+ bool: {
+ filter: responsesFilters,
+ },
+ },
+ },
+ };
+
+ let actionResponses: ApiResponse, unknown>;
+ try {
+ const esClient = context.core.elasticsearch.client.asCurrentUser;
+ actionResponses = await esClient.search(responsesSearchQuery, queryOptions);
+ } catch (error) {
+ logger.error(error);
+ throw error;
+ }
+ return actionResponses;
+};
+
+const matchesIndexPattern = ({
+ regexPattern,
+ index,
+}: {
+ regexPattern: RegExp;
+ index: string;
+}): boolean => regexPattern.test(index);
diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/index.ts b/x-pack/plugins/security_solution/server/endpoint/utils/index.ts
index 34cabf79aff0e..6c40073f8c654 100644
--- a/x-pack/plugins/security_solution/server/endpoint/utils/index.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/utils/index.ts
@@ -7,3 +7,5 @@
export * from './fleet_agent_status_to_endpoint_host_status';
export * from './wrap_errors';
+export * from './audit_log_helpers';
+export * from './yes_no_data_stream';
diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts
new file mode 100644
index 0000000000000..d2894c8c64c14
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 {
+ elasticsearchServiceMock,
+ savedObjectsClientMock,
+ loggingSystemMock,
+} from 'src/core/server/mocks';
+import { SecuritySolutionRequestHandlerContext } from '../../types';
+import { createRouteHandlerContext } from '../mocks';
+import {
+ doLogsEndpointActionDsExists,
+ doesLogsEndpointActionsIndexExist,
+} from './yes_no_data_stream';
+
+describe('Accurately answers if index template for data stream exists', () => {
+ let ctxt: jest.Mocked;
+
+ beforeEach(() => {
+ ctxt = createRouteHandlerContext(
+ elasticsearchServiceMock.createScopedClusterClient(),
+ savedObjectsClientMock.create()
+ );
+ });
+
+ const mockEsApiResponse = (response: { body: boolean; statusCode: number }) => {
+ return jest.fn().mockImplementationOnce(() => Promise.resolve(response));
+ };
+
+ it('Returns FALSE for a non-existent data stream index template', async () => {
+ ctxt.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = mockEsApiResponse({
+ body: false,
+ statusCode: 404,
+ });
+ const doesItExist = await doLogsEndpointActionDsExists({
+ context: ctxt,
+ logger: loggingSystemMock.create().get('host-isolation'),
+ dataStreamName: '.test-stream.name',
+ });
+ expect(doesItExist).toBeFalsy();
+ });
+
+ it('Returns TRUE for an existing index', async () => {
+ ctxt.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = mockEsApiResponse({
+ body: true,
+ statusCode: 200,
+ });
+ const doesItExist = await doLogsEndpointActionDsExists({
+ context: ctxt,
+ logger: loggingSystemMock.create().get('host-isolation'),
+ dataStreamName: '.test-stream.name',
+ });
+ expect(doesItExist).toBeTruthy();
+ });
+});
+
+describe('Accurately answers if index exists', () => {
+ let ctxt: jest.Mocked;
+
+ beforeEach(() => {
+ ctxt = createRouteHandlerContext(
+ elasticsearchServiceMock.createScopedClusterClient(),
+ savedObjectsClientMock.create()
+ );
+ });
+
+ const mockEsApiResponse = (response: { body: boolean; statusCode: number }) => {
+ return jest.fn().mockImplementationOnce(() => Promise.resolve(response));
+ };
+
+ it('Returns FALSE for a non-existent index', async () => {
+ ctxt.core.elasticsearch.client.asInternalUser.indices.exists = mockEsApiResponse({
+ body: false,
+ statusCode: 404,
+ });
+ const doesItExist = await doesLogsEndpointActionsIndexExist({
+ context: ctxt,
+ logger: loggingSystemMock.create().get('host-isolation'),
+ indexName: '.test-index.name-default',
+ });
+ expect(doesItExist).toBeFalsy();
+ });
+
+ it('Returns TRUE for an existing index', async () => {
+ ctxt.core.elasticsearch.client.asInternalUser.indices.exists = mockEsApiResponse({
+ body: true,
+ statusCode: 200,
+ });
+ const doesItExist = await doesLogsEndpointActionsIndexExist({
+ context: ctxt,
+ logger: loggingSystemMock.create().get('host-isolation'),
+ indexName: '.test-index.name-default',
+ });
+ expect(doesItExist).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts
new file mode 100644
index 0000000000000..dea2e46c3c258
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts
@@ -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 { Logger } from 'src/core/server';
+import { SecuritySolutionRequestHandlerContext } from '../../types';
+
+export const doLogsEndpointActionDsExists = async ({
+ context,
+ logger,
+ dataStreamName,
+}: {
+ context: SecuritySolutionRequestHandlerContext;
+ logger: Logger;
+ dataStreamName: string;
+}): Promise => {
+ try {
+ const esClient = context.core.elasticsearch.client.asInternalUser;
+ const doesIndexTemplateExist = await esClient.indices.existsIndexTemplate({
+ name: dataStreamName,
+ });
+ return doesIndexTemplateExist.statusCode === 404 ? false : true;
+ } catch (error) {
+ const errorType = error?.type ?? '';
+ if (errorType !== 'resource_not_found_exception') {
+ logger.error(error);
+ throw error;
+ }
+ return false;
+ }
+};
+
+export const doesLogsEndpointActionsIndexExist = async ({
+ context,
+ logger,
+ indexName,
+}: {
+ context: SecuritySolutionRequestHandlerContext;
+ logger: Logger;
+ indexName: string;
+}): Promise => {
+ try {
+ const esClient = context.core.elasticsearch.client.asInternalUser;
+ const doesIndexExist = await esClient.indices.exists({
+ index: indexName,
+ });
+ return doesIndexExist.statusCode === 404 ? false : true;
+ } catch (error) {
+ const errorType = error?.type ?? '';
+ if (errorType !== 'index_not_found_exception') {
+ logger.error(error);
+ throw error;
+ }
+ return false;
+ }
+};
From 78c2a33243690975b048da2aebaf925a93705972 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 05:51:56 -0400
Subject: [PATCH 10/18] [Security Solution][Endpoint] Fix unhandled promise
rejections in skipped tests (#115354) (#115399)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fix errors and comment code in middleware (pending to fix this)
* Fix endpoint list middleware test
* Fix policy TA layout test
* Fix test returning missing promise
Co-authored-by: David Sánchez
---
.../search_exceptions.test.tsx | 1 -
.../management/pages/endpoint_hosts/mocks.ts | 39 ++++++-------
.../endpoint_hosts/store/middleware.test.ts | 4 +-
.../policy_trusted_apps_layout.test.tsx | 56 +++++++++++++------
4 files changed, 59 insertions(+), 41 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx
index d7db249475df7..084978d35d03a 100644
--- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx
@@ -20,7 +20,6 @@ jest.mock('../../../common/components/user_privileges/use_endpoint_privileges');
let onSearchMock: jest.Mock;
const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
-// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699
describe('Search exceptions', () => {
let appTestContext: AppContextTestRender;
let renderResult: ReturnType;
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
index e0b5837c2f78a..c724773593f53 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
@@ -122,30 +122,27 @@ export const endpointActivityLogHttpMock =
const responseData = fleetActionGenerator.generateResponse({
agent_id: endpointMetadata.agent.id,
});
-
return {
- body: {
- page: 1,
- pageSize: 50,
- startDate: 'now-1d',
- endDate: 'now',
- data: [
- {
- type: 'response',
- item: {
- id: '',
- data: responseData,
- },
+ page: 1,
+ pageSize: 50,
+ startDate: 'now-1d',
+ endDate: 'now',
+ data: [
+ {
+ type: 'response',
+ item: {
+ id: '',
+ data: responseData,
},
- {
- type: 'action',
- item: {
- id: '',
- data: actionData,
- },
+ },
+ {
+ type: 'action',
+ item: {
+ id: '',
+ data: actionData,
},
- ],
- },
+ },
+ ],
};
},
},
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
index 43fa4e104067f..81c4dc6f2f7de 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
@@ -61,8 +61,7 @@ jest.mock('../../../../common/lib/kibana');
type EndpointListStore = Store, Immutable>;
-// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699
-describe.skip('endpoint list middleware', () => {
+describe('endpoint list middleware', () => {
const getKibanaServicesMock = KibanaServices.get as jest.Mock;
let fakeCoreStart: jest.Mocked;
let depsStart: DepsStartMock;
@@ -390,7 +389,6 @@ describe.skip('endpoint list middleware', () => {
it('should call get Activity Log API with correct paging options', async () => {
dispatchUserChangedUrl();
-
const updatePagingDispatched = waitForAction('endpointDetailsActivityLogUpdatePaging');
dispatchGetActivityLogPaging({ page: 3 });
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx
index e519d19d60fdc..43e19c00bcc8e 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx
@@ -19,20 +19,14 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../..
import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing';
import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data';
import { policyListApiPathHandlers } from '../../../store/test_mock_utils';
-import { licenseService } from '../../../../../../common/hooks/use_license';
+import {
+ EndpointPrivileges,
+ useEndpointPrivileges,
+} from '../../../../../../common/components/user_privileges/use_endpoint_privileges';
jest.mock('../../../../trusted_apps/service');
-jest.mock('../../../../../../common/hooks/use_license', () => {
- const licenseServiceInstance = {
- isPlatinumPlus: jest.fn(),
- };
- return {
- licenseService: licenseServiceInstance,
- useLicense: () => {
- return licenseServiceInstance;
- },
- };
-});
+jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges');
+const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
let mockedContext: AppContextTestRender;
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];
@@ -42,8 +36,17 @@ let coreStart: AppContextTestRender['coreStart'];
let http: typeof coreStart.http;
const generator = new EndpointDocGenerator();
-// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699
-describe.skip('Policy trusted apps layout', () => {
+describe('Policy trusted apps layout', () => {
+ const loadedUserEndpointPrivilegesState = (
+ endpointOverrides: Partial = {}
+ ): EndpointPrivileges => ({
+ loading: false,
+ canAccessFleet: true,
+ canAccessEndpointManagement: true,
+ isPlatinumPlus: true,
+ ...endpointOverrides,
+ });
+
beforeEach(() => {
mockedContext = createAppRootMockRenderer();
http = mockedContext.coreStart.http;
@@ -59,6 +62,14 @@ describe.skip('Policy trusted apps layout', () => {
});
}
+ // GET Agent status for agent policy
+ if (path === '/api/fleet/agent-status') {
+ return Promise.resolve({
+ results: { events: 0, total: 5, online: 3, error: 1, offline: 1 },
+ success: true,
+ });
+ }
+
// Get package data
// Used in tests that route back to the list
if (policyListApiHandlers[path]) {
@@ -78,6 +89,10 @@ describe.skip('Policy trusted apps layout', () => {
render = () => mockedContext.render();
});
+ afterAll(() => {
+ mockUseEndpointPrivileges.mockReset();
+ });
+
afterEach(() => reactTestingLibrary.cleanup());
it('should renders layout with no existing TA data', async () => {
@@ -121,7 +136,11 @@ describe.skip('Policy trusted apps layout', () => {
});
it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => {
- (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
+ mockUseEndpointPrivileges.mockReturnValue(
+ loadedUserEndpointPrivilegesState({
+ isPlatinumPlus: false,
+ })
+ );
const component = render();
mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234'));
@@ -133,8 +152,13 @@ describe.skip('Policy trusted apps layout', () => {
});
expect(component.queryByTestId('assign-ta-button')).toBeNull();
});
+
it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => {
- (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
+ mockUseEndpointPrivileges.mockReturnValue(
+ loadedUserEndpointPrivilegesState({
+ isPlatinumPlus: false,
+ })
+ );
TrustedAppsHttpServiceMock.mockImplementation(() => {
return {
getTrustedAppsList: () => getMockListResponse(),
From 2d953bc83c34cb427030815bf976cc7d1660f594 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 06:12:26 -0400
Subject: [PATCH 11/18] Fix alerts Count table title overflow wraps prematurely
(#115364) (#115510)
Co-authored-by: Pablo Machado
---
.../components/alerts_kpis/alerts_count_panel/index.tsx | 2 +-
.../components/alerts_kpis/alerts_histogram_panel/index.tsx | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx
index 29324d186784e..c8d45ca67068a 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx
@@ -94,7 +94,7 @@ export const AlertsCountPanel = memo(
{i18n.COUNT_TABLE_TITLE}}
titleSize="s"
hideSubtitle
>
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx
index 0613c619d89b9..07fa81f27684c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx
@@ -257,7 +257,11 @@ export const AlertsHistogramPanel = memo(
}, [showLinkToAlerts, goToDetectionEngine, formatUrl]);
const titleText = useMemo(
- () => (onlyField == null ? title : i18n.TOP(onlyField)),
+ () => (
+
+ {onlyField == null ? title : i18n.TOP(onlyField)}
+
+ ),
[onlyField, title]
);
From 65786cf25123381e5124c76687f7b0a15600b68e Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 06:17:37 -0400
Subject: [PATCH 12/18] [Security Solution][Endpoint] Change Trusted Apps to
use `item_id` as its identifier and Enable Trusted Apps filtering by id in
the UI (#115276) (#115509)
* Add `item_id` to list of searchable fields
* trusted apps api changes to use `item_id` instead of SO `id`
* Change Policy Details Trusted App "View all details" action URL to show TA list filtered by the TA id
Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
---
.../list/policy_trusted_apps_list.test.tsx | 2 +-
.../list/policy_trusted_apps_list.tsx | 2 +-
.../pages/trusted_apps/constants.ts | 1 +
.../routes/trusted_apps/handlers.test.ts | 5 +-
.../endpoint/routes/trusted_apps/mapping.ts | 2 +-
.../routes/trusted_apps/service.test.ts | 24 +++++-
.../endpoint/routes/trusted_apps/service.ts | 77 +++++++++++++------
7 files changed, 84 insertions(+), 29 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx
index a8d3cc1505463..b5bfc16db2899 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx
@@ -207,7 +207,7 @@ describe('when rendering the PolicyTrustedAppsList', () => {
expect(appTestContext.coreStart.application.navigateToApp).toHaveBeenCalledWith(
APP_ID,
expect.objectContaining({
- path: '/administration/trusted_apps?show=edit&id=89f72d8a-05b5-4350-8cad-0dc3661d6e67',
+ path: '/administration/trusted_apps?filter=89f72d8a-05b5-4350-8cad-0dc3661d6e67',
})
);
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx
index f6afd9d502486..7b1f8753831c8 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx
@@ -113,7 +113,7 @@ export const PolicyTrustedAppsList = memo(
for (const trustedApp of trustedAppItems) {
const isGlobal = trustedApp.effectScope.type === 'global';
- const viewUrlPath = getTrustedAppsListPath({ id: trustedApp.id, show: 'edit' });
+ const viewUrlPath = getTrustedAppsListPath({ filter: trustedApp.id });
const assignedPoliciesMenuItems: ArtifactEntryCollapsibleCardProps['policies'] =
trustedApp.effectScope.type === 'global'
? undefined
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts
index 0602ae18c1408..beefb8587d787 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts
@@ -8,6 +8,7 @@
export const SEARCHABLE_FIELDS: Readonly = [
`name`,
`description`,
+ 'item_id',
`entries.value`,
`entries.entries.value`,
];
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts
index 547c1f6a2e5ff..614ad4fb548ea 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts
@@ -110,7 +110,7 @@ const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' }
const packagePolicyClient =
createPackagePolicyServiceMock() as jest.Mocked;
-describe('handlers', () => {
+describe('TrustedApps API Handlers', () => {
beforeEach(() => {
packagePolicyClient.getByIDs.mockReset();
});
@@ -195,6 +195,7 @@ describe('handlers', () => {
const mockResponse = httpServerMock.createResponseFactory();
exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null);
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
await deleteTrustedAppHandler(
createHandlerContextMock(),
@@ -582,7 +583,7 @@ describe('handlers', () => {
});
it('should return 404 if trusted app does not exist', async () => {
- exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null);
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
await updateHandler(
createHandlerContextMock(),
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
index 2c085c14db009..08c1a3a809d4a 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
@@ -122,7 +122,7 @@ export const exceptionListItemToTrustedApp = (
const grouped = entriesToConditionEntriesMap(exceptionListItem.entries);
return {
- id: exceptionListItem.id,
+ id: exceptionListItem.item_id,
version: exceptionListItem._version || '',
name: exceptionListItem.name,
description: exceptionListItem.description,
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts
index dce84df735929..c57416ff1c974 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts
@@ -85,9 +85,10 @@ const TRUSTED_APP: TrustedApp = {
],
};
-describe('service', () => {
+describe('TrustedApps service', () => {
beforeEach(() => {
exceptionsListClient.deleteExceptionListItem.mockReset();
+ exceptionsListClient.getExceptionListItem.mockReset();
exceptionsListClient.createExceptionListItem.mockReset();
exceptionsListClient.findExceptionListItem.mockReset();
exceptionsListClient.createTrustedAppsList.mockReset();
@@ -96,6 +97,7 @@ describe('service', () => {
describe('deleteTrustedApp', () => {
it('should delete existing trusted app', async () => {
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
exceptionsListClient.deleteExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
expect(await deleteTrustedApp(exceptionsListClient, { id: '123' })).toBeUndefined();
@@ -107,6 +109,7 @@ describe('service', () => {
});
it('should throw for non existing trusted app', async () => {
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null);
await expect(deleteTrustedApp(exceptionsListClient, { id: '123' })).rejects.toBeInstanceOf(
@@ -393,7 +396,7 @@ describe('service', () => {
});
it('should throw a Not Found error if trusted app is not found prior to making update', async () => {
- exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null);
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
await expect(
updateTrustedApp(
exceptionsListClient,
@@ -489,5 +492,22 @@ describe('service', () => {
TrustedAppNotFoundError
);
});
+
+ it('should try to find trusted app by `itemId` and then by `id`', async () => {
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
+ await getTrustedApp(exceptionsListClient, '123').catch(() => Promise.resolve());
+
+ expect(exceptionsListClient.getExceptionListItem).toHaveBeenCalledTimes(2);
+ expect(exceptionsListClient.getExceptionListItem).toHaveBeenNthCalledWith(1, {
+ itemId: '123',
+ id: undefined,
+ namespaceType: 'agnostic',
+ });
+ expect(exceptionsListClient.getExceptionListItem).toHaveBeenNthCalledWith(2, {
+ itemId: undefined,
+ id: '123',
+ namespaceType: 'agnostic',
+ });
+ });
});
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
index 856a615c1ffa2..7a4b2372ece8f 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
@@ -15,13 +15,13 @@ import {
DeleteTrustedAppsRequestParams,
GetOneTrustedAppResponse,
GetTrustedAppsListRequest,
- GetTrustedAppsSummaryResponse,
GetTrustedAppsListResponse,
+ GetTrustedAppsSummaryRequest,
+ GetTrustedAppsSummaryResponse,
PostTrustedAppCreateRequest,
PostTrustedAppCreateResponse,
PutTrustedAppUpdateRequest,
PutTrustedAppUpdateResponse,
- GetTrustedAppsSummaryRequest,
TrustedApp,
} from '../../../../common/endpoint/types';
@@ -33,8 +33,8 @@ import {
} from './mapping';
import {
TrustedAppNotFoundError,
- TrustedAppVersionConflictError,
TrustedAppPolicyNotExistsError,
+ TrustedAppVersionConflictError,
} from './errors';
import { PackagePolicyServiceInterface } from '../../../../../fleet/server';
import { PackagePolicy } from '../../../../../fleet/common';
@@ -87,30 +87,61 @@ const isUserTryingToModifyEffectScopeWithoutPermissions = (
}
};
-export const deleteTrustedApp = async (
+/**
+ * Attempts to first fine the ExceptionItem using `item_id` and if not found, then a second attempt wil be done
+ * against the Saved Object `id`.
+ * @param exceptionsListClient
+ * @param id
+ */
+export const findTrustedAppExceptionItemByIdOrItemId = async (
exceptionsListClient: ExceptionListClient,
- { id }: DeleteTrustedAppsRequestParams
-) => {
- const exceptionListItem = await exceptionsListClient.deleteExceptionListItem({
- id,
+ id: string
+): Promise => {
+ const trustedAppExceptionItem = await exceptionsListClient.getExceptionListItem({
+ itemId: id,
+ id: undefined,
+ namespaceType: 'agnostic',
+ });
+
+ if (trustedAppExceptionItem) {
+ return trustedAppExceptionItem;
+ }
+
+ return exceptionsListClient.getExceptionListItem({
itemId: undefined,
+ id,
namespaceType: 'agnostic',
});
+};
- if (!exceptionListItem) {
+export const deleteTrustedApp = async (
+ exceptionsListClient: ExceptionListClient,
+ { id }: DeleteTrustedAppsRequestParams
+): Promise => {
+ const trustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId(
+ exceptionsListClient,
+ id
+ );
+
+ if (!trustedAppExceptionItem) {
throw new TrustedAppNotFoundError(id);
}
+
+ await exceptionsListClient.deleteExceptionListItem({
+ id: trustedAppExceptionItem.id,
+ itemId: undefined,
+ namespaceType: 'agnostic',
+ });
};
export const getTrustedApp = async (
exceptionsListClient: ExceptionListClient,
id: string
): Promise => {
- const trustedAppExceptionItem = await exceptionsListClient.getExceptionListItem({
- itemId: '',
- id,
- namespaceType: 'agnostic',
- });
+ const trustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId(
+ exceptionsListClient,
+ id
+ );
if (!trustedAppExceptionItem) {
throw new TrustedAppNotFoundError(id);
@@ -189,19 +220,18 @@ export const updateTrustedApp = async (
updatedTrustedApp: PutTrustedAppUpdateRequest,
isAtLeastPlatinum: boolean
): Promise => {
- const currentTrustedApp = await exceptionsListClient.getExceptionListItem({
- itemId: '',
- id,
- namespaceType: 'agnostic',
- });
+ const currentTrustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId(
+ exceptionsListClient,
+ id
+ );
- if (!currentTrustedApp) {
+ if (!currentTrustedAppExceptionItem) {
throw new TrustedAppNotFoundError(id);
}
if (
isUserTryingToModifyEffectScopeWithoutPermissions(
- exceptionListItemToTrustedApp(currentTrustedApp),
+ exceptionListItemToTrustedApp(currentTrustedAppExceptionItem),
updatedTrustedApp,
isAtLeastPlatinum
)
@@ -226,7 +256,10 @@ export const updateTrustedApp = async (
try {
updatedTrustedAppExceptionItem = await exceptionsListClient.updateExceptionListItem(
- updatedTrustedAppToUpdateExceptionListItemOptions(currentTrustedApp, updatedTrustedApp)
+ updatedTrustedAppToUpdateExceptionListItemOptions(
+ currentTrustedAppExceptionItem,
+ updatedTrustedApp
+ )
);
} catch (e) {
if (e?.output?.statusCode === 409) {
From 8fec45c45972495ff9f8e95b555509501c2eeadc Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 06:47:44 -0400
Subject: [PATCH 13/18] [Discover] Enable description for saved search modal
(#114257) (#115514)
* [Discover] enable description for saved search
* [Discover] remove i18n translations for removed description
* [Discover] apply Tim's suggestion
* [Discover] update snapshot
* [Discover] reorder top nav buttons in tests
* [Description] fix description save action
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com>
---
.../components/top_nav/discover_topnav.test.tsx | 2 +-
.../components/top_nav/get_top_nav_links.test.ts | 16 +++++++++-------
.../main/components/top_nav/get_top_nav_links.ts | 4 +++-
.../main/components/top_nav/on_save_search.tsx | 10 +++++-----
.../embeddable/saved_search_embeddable.tsx | 4 ++++
.../plugins/translations/translations/ja-JP.json | 1 -
.../plugins/translations/translations/zh-CN.json | 1 -
7 files changed, 22 insertions(+), 16 deletions(-)
diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx
index 4b572f6e348b8..808346b53304c 100644
--- a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx
@@ -42,7 +42,7 @@ describe('Discover topnav component', () => {
const props = getProps(true);
const component = shallowWithIntl();
const topMenuConfig = component.props().config.map((obj: TopNavMenuData) => obj.id);
- expect(topMenuConfig).toEqual(['options', 'new', 'save', 'open', 'share', 'inspect']);
+ expect(topMenuConfig).toEqual(['options', 'new', 'open', 'share', 'inspect', 'save']);
});
test('generated config of TopNavMenu config is correct when no discover save permissions are assigned', () => {
diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts
index d31ac6e0f2fea..20c5b9bae332d 100644
--- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts
+++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts
@@ -53,13 +53,6 @@ test('getTopNavLinks result', () => {
"run": [Function],
"testId": "discoverNewButton",
},
- Object {
- "description": "Save Search",
- "id": "save",
- "label": "Save",
- "run": [Function],
- "testId": "discoverSaveButton",
- },
Object {
"description": "Open Saved Search",
"id": "open",
@@ -81,6 +74,15 @@ test('getTopNavLinks result', () => {
"run": [Function],
"testId": "openInspectorButton",
},
+ Object {
+ "description": "Save Search",
+ "emphasize": true,
+ "iconType": "save",
+ "id": "save",
+ "label": "Save",
+ "run": [Function],
+ "testId": "discoverSaveButton",
+ },
]
`);
});
diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts
index 81be662470306..44d2999947f41 100644
--- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts
+++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts
@@ -76,6 +76,8 @@ export const getTopNavLinks = ({
defaultMessage: 'Save Search',
}),
testId: 'discoverSaveButton',
+ iconType: 'save',
+ emphasize: true,
run: () => onSaveSearch({ savedSearch, services, indexPattern, navigateTo, state }),
};
@@ -153,9 +155,9 @@ export const getTopNavLinks = ({
return [
...(services.capabilities.advancedSettings.save ? [options] : []),
newSearch,
- ...(services.capabilities.discover.save ? [saveSearch] : []),
openSearch,
shareSearch,
inspectSearch,
+ ...(services.capabilities.discover.save ? [saveSearch] : []),
];
};
diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx
index 18766b5df7f33..25b04e12c650a 100644
--- a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx
@@ -98,16 +98,19 @@ export async function onSaveSearch({
const onSave = async ({
newTitle,
newCopyOnSave,
+ newDescription,
isTitleDuplicateConfirmed,
onTitleDuplicate,
}: {
newTitle: string;
newCopyOnSave: boolean;
+ newDescription: string;
isTitleDuplicateConfirmed: boolean;
onTitleDuplicate: () => void;
}) => {
const currentTitle = savedSearch.title;
savedSearch.title = newTitle;
+ savedSearch.description = newDescription;
const saveOptions: SaveSavedSearchOptions = {
onTitleDuplicate,
copyOnSave: newCopyOnSave,
@@ -136,14 +139,11 @@ export async function onSaveSearch({
onClose={() => {}}
title={savedSearch.title ?? ''}
showCopyOnSave={!!savedSearch.id}
+ description={savedSearch.description}
objectType={i18n.translate('discover.localMenu.saveSaveSearchObjectType', {
defaultMessage: 'search',
})}
- description={i18n.translate('discover.localMenu.saveSaveSearchDescription', {
- defaultMessage:
- 'Save your Discover search so you can use it in visualizations and dashboards',
- })}
- showDescription={false}
+ showDescription={true}
/>
);
showSaveModal(saveModal, services.core.i18n.Context);
diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx
index 8849806cf5959..89c47559d7b4c 100644
--- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx
+++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx
@@ -402,6 +402,10 @@ export class SavedSearchEmbeddable
return this.inspectorAdapters;
}
+ public getDescription() {
+ return this.savedSearch.description;
+ }
+
public destroy() {
super.destroy();
if (this.searchProps) {
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 113eae5d08e07..f909a03909e3f 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -2534,7 +2534,6 @@
"discover.localMenu.openSavedSearchDescription": "保存された検索を開きます",
"discover.localMenu.openTitle": "開く",
"discover.localMenu.optionsDescription": "オプション",
- "discover.localMenu.saveSaveSearchDescription": "ビジュアライゼーションとダッシュボードで使用できるように Discover の検索を保存します",
"discover.localMenu.saveSaveSearchObjectType": "検索",
"discover.localMenu.saveSearchDescription": "検索を保存します",
"discover.localMenu.saveTitle": "保存",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 15933599699eb..4de407cf8e464 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -2560,7 +2560,6 @@
"discover.localMenu.openSavedSearchDescription": "打开已保存搜索",
"discover.localMenu.openTitle": "打开",
"discover.localMenu.optionsDescription": "选项",
- "discover.localMenu.saveSaveSearchDescription": "保存您的 Discover 搜索,以便可以在可视化和仪表板中使用该搜索",
"discover.localMenu.saveSaveSearchObjectType": "搜索",
"discover.localMenu.saveSearchDescription": "保存搜索",
"discover.localMenu.saveTitle": "保存",
From 44e0e53877b16723cf792e2ebea0e345c6de0231 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 07:10:02 -0400
Subject: [PATCH 14/18] [Security Solution][Rules] Halt Indicator Match
execution after interval has passed (#115288) (#115517)
* Throw an error to stop execution if IM rule has exceeded its interval
* Extract and unit test our timeout validation
* Add integration test around timeout behavior
Configures a very slow rule to trigger a timeout and assert the
corresponding failure.
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Ryland Herrick
---
.../threat_mapping/create_threat_signals.ts | 6 ++-
.../signals/threat_mapping/utils.test.ts | 23 ++++++++++
.../signals/threat_mapping/utils.ts | 21 +++++++++
.../tests/create_threat_matching.ts | 46 +++++++++++++++++++
4 files changed, 95 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
index 169a820392a6e..677a2028acdf7 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
@@ -11,7 +11,7 @@ import { getThreatList, getThreatListCount } from './get_threat_list';
import { CreateThreatSignalsOptions } from './types';
import { createThreatSignal } from './create_threat_signal';
import { SearchAfterAndBulkCreateReturnType } from '../types';
-import { combineConcurrentResults } from './utils';
+import { buildExecutionIntervalValidator, combineConcurrentResults } from './utils';
import { buildThreatEnrichment } from './build_threat_enrichment';
export const createThreatSignals = async ({
@@ -46,6 +46,9 @@ export const createThreatSignals = async ({
const params = ruleSO.attributes.params;
logger.debug(buildRuleMessage('Indicator matching rule starting'));
const perPage = concurrentSearches * itemsPerSearch;
+ const verifyExecutionCanProceed = buildExecutionIntervalValidator(
+ ruleSO.attributes.schedule.interval
+ );
let results: SearchAfterAndBulkCreateReturnType = {
success: true,
@@ -99,6 +102,7 @@ export const createThreatSignals = async ({
});
while (threatList.hits.hits.length !== 0) {
+ verifyExecutionCanProceed();
const chunks = chunk(itemsPerSearch, threatList.hits.hits);
logger.debug(buildRuleMessage(`${chunks.length} concurrent indicator searches are starting.`));
const concurrentSearchesPerformed = chunks.map>(
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
index ec826b44023f6..f029b02127b08 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
@@ -10,6 +10,7 @@ import { sampleSignalHit } from '../__mocks__/es_results';
import { ThreatMatchNamedQuery } from './types';
import {
+ buildExecutionIntervalValidator,
calculateAdditiveMax,
calculateMax,
calculateMaxLookBack,
@@ -712,4 +713,26 @@ describe('utils', () => {
});
});
});
+
+ describe('buildExecutionIntervalValidator', () => {
+ it('succeeds if the validator is called within the specified interval', () => {
+ const validator = buildExecutionIntervalValidator('1m');
+ expect(() => validator()).not.toThrowError();
+ });
+
+ it('throws an error if the validator is called after the specified interval', async () => {
+ const validator = buildExecutionIntervalValidator('1s');
+
+ await new Promise((r) => setTimeout(r, 1001));
+ expect(() => validator()).toThrowError(
+ 'Current rule execution has exceeded its allotted interval (1s) and has been stopped.'
+ );
+ });
+
+ it('throws an error if the interval cannot be parsed', () => {
+ expect(() => buildExecutionIntervalValidator('badString')).toThrowError(
+ 'Unable to parse rule interval (badString); stopping rule execution since allotted duration is undefined'
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
index 4d9fda43f032e..99f6609faec91 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
@@ -5,7 +5,10 @@
* 2.0.
*/
+import moment from 'moment';
+
import { SearchAfterAndBulkCreateReturnType, SignalSourceHit } from '../types';
+import { parseInterval } from '../utils';
import { ThreatMatchNamedQuery } from './types';
/**
@@ -146,3 +149,21 @@ export const decodeThreatMatchNamedQuery = (encoded: string): ThreatMatchNamedQu
export const extractNamedQueries = (hit: SignalSourceHit): ThreatMatchNamedQuery[] =>
hit.matched_queries?.map((match) => decodeThreatMatchNamedQuery(match)) ?? [];
+
+export const buildExecutionIntervalValidator: (interval: string) => () => void = (interval) => {
+ const intervalDuration = parseInterval(interval);
+
+ if (intervalDuration == null) {
+ throw new Error(
+ `Unable to parse rule interval (${interval}); stopping rule execution since allotted duration is undefined.`
+ );
+ }
+
+ const executionEnd = moment().add(intervalDuration);
+ return () => {
+ if (moment().isAfter(executionEnd)) {
+ const message = `Current rule execution has exceeded its allotted interval (${interval}) and has been stopped.`;
+ throw new Error(message);
+ }
+ };
+};
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts
index 0aad3c699805a..223529fce54f6 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts
@@ -411,6 +411,52 @@ export default ({ getService }: FtrProviderContext) => {
expect(signalsOpen.hits.hits.length).equal(0);
});
+ describe('timeout behavior', () => {
+ it('will return an error if a rule execution exceeds the rule interval', async () => {
+ const rule: CreateRulesSchema = {
+ description: 'Detecting root and admin users',
+ name: 'Query with a short interval',
+ severity: 'high',
+ index: ['auditbeat-*'],
+ type: 'threat_match',
+ risk_score: 55,
+ language: 'kuery',
+ rule_id: 'rule-1',
+ from: '1900-01-01T00:00:00.000Z',
+ query: '*:*',
+ threat_query: '*:*', // broad query to take more time
+ threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity
+ threat_mapping: [
+ {
+ entries: [
+ {
+ field: 'host.name',
+ value: 'host.name',
+ type: 'mapping',
+ },
+ ],
+ },
+ ],
+ threat_filters: [],
+ concurrent_searches: 1,
+ interval: '1s', // short interval
+ items_per_search: 1, // iterate only 1 threat item per loop to ensure we're slow
+ };
+
+ const { id } = await createRule(supertest, rule);
+ await waitForRuleSuccessOrStatus(supertest, id, 'failed');
+
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`)
+ .set('kbn-xsrf', 'true')
+ .send({ ids: [id] })
+ .expect(200);
+ expect(body[id].current_status.last_failure_message).to.contain(
+ 'execution has exceeded its allotted interval'
+ );
+ });
+ });
+
describe('indicator enrichment', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/filebeat/threat_intel');
From 0c86129e4a21838983b23bdbc778d2f20d2751c0 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 07:37:34 -0400
Subject: [PATCH 15/18] [Security Solution][Detections] Hide building block
rules in "Security/Overview" (#105611) (#115521)
* Hide building block rules in "Security/Overview"
* Add Cypress tests for alerts generated by building block rules
Co-authored-by: Dmitry Shevchenko
Co-authored-by: Georgii Gorbachev
Co-authored-by: Dmitry Shevchenko
---
.../building_block_alerts.spec.ts | 40 +++++++++++++++++++
.../security_solution/cypress/objects/rule.ts | 20 ++++++++++
.../cypress/screens/overview.ts | 2 +
.../cypress/tasks/api_calls/rules.ts | 1 +
.../components/signals_by_category/index.tsx | 15 +++++--
.../use_filters_for_signals_by_category.ts | 37 +++++++++++++++++
6 files changed, 111 insertions(+), 4 deletions(-)
create mode 100644 x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts
create mode 100644 x-pack/plugins/security_solution/public/overview/components/signals_by_category/use_filters_for_signals_by_category.ts
diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts
new file mode 100644
index 0000000000000..262ffe8163e57
--- /dev/null
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { getBuildingBlockRule } from '../../objects/rule';
+import { OVERVIEW_ALERTS_HISTOGRAM } from '../../screens/overview';
+import { OVERVIEW } from '../../screens/security_header';
+import { goToRuleDetails } from '../../tasks/alerts_detection_rules';
+import { createCustomRuleActivated } from '../../tasks/api_calls/rules';
+import { cleanKibana } from '../../tasks/common';
+import { waitForAlertsToPopulate, waitForTheRuleToBeExecuted } from '../../tasks/create_new_rule';
+import { loginAndWaitForPage } from '../../tasks/login';
+import { navigateFromHeaderTo } from '../../tasks/security_header';
+import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';
+
+const EXPECTED_NUMBER_OF_ALERTS = 16;
+
+describe('Alerts generated by building block rules', () => {
+ beforeEach(() => {
+ cleanKibana();
+ });
+
+ it('Alerts should be visible on the Rule Detail page and not visible on the Overview page', () => {
+ createCustomRuleActivated(getBuildingBlockRule());
+ loginAndWaitForPage(DETECTIONS_RULE_MANAGEMENT_URL);
+ goToRuleDetails();
+ waitForTheRuleToBeExecuted();
+
+ // Check that generated events are visible on the Details page
+ waitForAlertsToPopulate(EXPECTED_NUMBER_OF_ALERTS);
+
+ navigateFromHeaderTo(OVERVIEW);
+
+ // Check that generated events are hidden on the Overview page
+ cy.get(OVERVIEW_ALERTS_HISTOGRAM).should('contain.text', 'No data to display');
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts
index 4b061865d632b..27973854097db 100644
--- a/x-pack/plugins/security_solution/cypress/objects/rule.ts
+++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts
@@ -58,6 +58,7 @@ export interface CustomRule {
lookBack: Interval;
timeline: CompleteTimeline;
maxSignals: number;
+ buildingBlockType?: string;
}
export interface ThresholdRule extends CustomRule {
@@ -188,6 +189,25 @@ export const getNewRule = (): CustomRule => ({
maxSignals: 100,
});
+export const getBuildingBlockRule = (): CustomRule => ({
+ customQuery: 'host.name: *',
+ index: getIndexPatterns(),
+ name: 'Building Block Rule Test',
+ description: 'The new rule description.',
+ severity: 'High',
+ riskScore: '17',
+ tags: ['test', 'newRule'],
+ referenceUrls: ['http://example.com/', 'https://example.com/'],
+ falsePositivesExamples: ['False1', 'False2'],
+ mitre: [getMitre1(), getMitre2()],
+ note: '# test markdown',
+ runsEvery: getRunsEvery(),
+ lookBack: getLookBack(),
+ timeline: getTimeline(),
+ maxSignals: 100,
+ buildingBlockType: 'default',
+});
+
export const getUnmappedRule = (): CustomRule => ({
customQuery: '*:*',
index: ['unmapped*'],
diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts
index 1376a39e5ee79..1945b7e3ce3e7 100644
--- a/x-pack/plugins/security_solution/cypress/screens/overview.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts
@@ -166,3 +166,5 @@ export const OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON =
export const OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT = `${OVERVIEW_RISKY_HOSTS_LINKS} [data-test-subj="header-panel-subtitle"]`;
export const OVERVIEW_RISKY_HOSTS_ENABLE_MODULE_BUTTON =
'[data-test-subj="risky-hosts-enable-module-button"]';
+
+export const OVERVIEW_ALERTS_HISTOGRAM = '[data-test-subj="alerts-histogram-panel"]';
diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
index 04ff0fcabc081..fd2838e5b3caa 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
@@ -114,6 +114,7 @@ export const createCustomRuleActivated = (
enabled: true,
tags: ['rule1'],
max_signals: maxSignals,
+ building_block_type: rule.buildingBlockType,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
failOnStatusCode: false,
diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx
index 321e6d00b5301..cbeb1464e1b41 100644
--- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx
@@ -7,19 +7,24 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
+import { Filter, Query } from '@kbn/es-query';
import { AlertsHistogramPanel } from '../../../detections/components/alerts_kpis/alerts_histogram_panel';
import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions';
-import { Filter, Query } from '../../../../../../../src/plugins/data/public';
+
import { InputsModelId } from '../../../common/store/inputs/constants';
-import * as i18n from '../../pages/translations';
import { UpdateDateRange } from '../../../common/components/charts/common';
+
import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types';
+import * as i18n from '../../pages/translations';
+
+import { useFiltersForSignalsByCategory } from './use_filters_for_signals_by_category';
+
interface Props {
combinedQueries?: string;
- filters?: Filter[];
+ filters: Filter[];
headerChildren?: React.ReactNode;
/** Override all defaults, and only display this field */
onlyField?: AlertsStackByField;
@@ -43,6 +48,8 @@ const SignalsByCategoryComponent: React.FC = ({
}) => {
const dispatch = useDispatch();
const { signalIndexName } = useSignalIndex();
+ const filtersForSignalsByCategory = useFiltersForSignalsByCategory(filters);
+
const updateDateRangeCallback = useCallback(
({ x }) => {
if (!x) {
@@ -63,7 +70,7 @@ const SignalsByCategoryComponent: React.FC = ({
return (
{
+ // TODO: Once we are past experimental phase this code should be removed
+ const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled');
+
+ const resultingFilters = useMemo(
+ () => [
+ ...baseFilters,
+ ...(ruleRegistryEnabled
+ ? buildShowBuildingBlockFilterRuleRegistry(SHOW_BUILDING_BLOCK_ALERTS) // TODO: Once we are past experimental phase this code should be removed
+ : buildShowBuildingBlockFilter(SHOW_BUILDING_BLOCK_ALERTS)),
+ ],
+ [baseFilters, ruleRegistryEnabled]
+ );
+
+ return resultingFilters;
+};
From f54724dc2b9f4597b137054864db3e63e6402144 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 07:37:47 -0400
Subject: [PATCH 16/18] [Unified Integrations] Clean up empty states, tutorial
links and routing to prefer unified integrations (#114911) (#115493)
Cleans up the integrations view and redirects all links to the integration manager.
Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com>
---
.../chrome/ui/header/collapsible_nav.tsx | 2 +-
.../__snapshots__/add_data.test.tsx.snap | 16 +-
.../components/add_data/add_data.test.tsx | 4 +-
.../components/add_data/add_data.tsx | 152 +++++++++---------
.../components/sample_data/index.tsx | 4 +-
.../components/tutorial_directory.js | 56 +------
.../public/application/components/welcome.tsx | 3 +-
src/plugins/home/public/index.ts | 1 -
src/plugins/home/public/services/index.ts | 1 -
.../home/public/services/tutorials/index.ts | 1 -
.../tutorials/tutorial_service.mock.ts | 1 -
.../tutorials/tutorial_service.test.tsx | 32 ----
.../services/tutorials/tutorial_service.ts | 18 ---
.../empty_index_list_prompt.tsx | 2 +-
.../__snapshots__/overview.test.tsx.snap | 110 +++----------
.../public/components/overview/overview.tsx | 12 +-
.../public/assets/elastic_beats_card_dark.svg | 1 -
.../assets/elastic_beats_card_light.svg | 1 -
.../__snapshots__/no_data_page.test.tsx.snap | 4 +-
.../elastic_agent_card.test.tsx.snap | 55 ++++++-
.../elastic_beats_card.test.tsx.snap | 70 --------
.../no_data_card/elastic_agent_card.test.tsx | 10 +-
.../no_data_card/elastic_agent_card.tsx | 44 ++++-
.../no_data_card/elastic_beats_card.test.tsx | 45 ------
.../no_data_card/elastic_beats_card.tsx | 66 --------
.../no_data_page/no_data_card/index.ts | 1 -
.../no_data_page/no_data_page.tsx | 14 +-
.../components/app/RumDashboard/RumHome.tsx | 8 +-
.../routing/templates/no_data_config.ts | 10 +-
.../epm/components/package_list_grid.tsx | 2 +-
.../components/home_integration/index.tsx | 8 -
.../tutorial_directory_header_link.tsx | 16 +-
.../tutorial_directory_notice.tsx | 147 -----------------
x-pack/plugins/fleet/public/plugin.ts | 7 +-
.../infra/public/pages/logs/page_content.tsx | 2 +-
.../infra/public/pages/logs/page_template.tsx | 6 +-
.../logs/stream/page_no_indices_content.tsx | 4 +-
.../infra/public/pages/metrics/index.tsx | 4 +-
.../metric_detail/components/invalid_node.tsx | 4 +-
.../public/pages/metrics/page_template.tsx | 9 +-
.../components/app/header/header_menu.tsx | 2 +-
.../public/utils/no_data_config.ts | 7 +-
.../security_solution/common/constants.ts | 2 +-
.../components/overview_empty/index.test.tsx | 12 +-
.../components/overview_empty/index.tsx | 50 ++----
.../translations/translations/ja-JP.json | 11 --
.../translations/translations/zh-CN.json | 11 --
x-pack/test/accessibility/apps/home.ts | 27 ----
48 files changed, 292 insertions(+), 783 deletions(-)
delete mode 100644 src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg
delete mode 100644 src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg
delete mode 100644 src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap
delete mode 100644 src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx
delete mode 100644 src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx
delete mode 100644 x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_notice.tsx
diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx
index ad590865b9e14..ccc0e17b655b1 100644
--- a/src/core/public/chrome/ui/header/collapsible_nav.tsx
+++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx
@@ -362,7 +362,7 @@ export function CollapsibleNav({
iconType="plusInCircleFilled"
>
{i18n.translate('core.ui.primaryNav.addData', {
- defaultMessage: 'Add data',
+ defaultMessage: 'Add integrations',
})}
diff --git a/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap b/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap
index 26b5697f008b6..de6beab31247a 100644
--- a/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap
+++ b/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap
@@ -17,7 +17,7 @@ exports[`AddData render 1`] = `
id="homDataAdd__title"
>
@@ -43,17 +43,25 @@ exports[`AddData render 1`] = `
grow={false}
>
diff --git a/src/plugins/home/public/application/components/add_data/add_data.test.tsx b/src/plugins/home/public/application/components/add_data/add_data.test.tsx
index 4018ae67c19ee..3aa51f89c7d67 100644
--- a/src/plugins/home/public/application/components/add_data/add_data.test.tsx
+++ b/src/plugins/home/public/application/components/add_data/add_data.test.tsx
@@ -27,7 +27,9 @@ beforeEach(() => {
jest.clearAllMocks();
});
-const applicationStartMock = {} as unknown as ApplicationStart;
+const applicationStartMock = {
+ capabilities: { navLinks: { integrations: true } },
+} as unknown as ApplicationStart;
const addBasePathMock = jest.fn((path: string) => (path ? path : 'path'));
diff --git a/src/plugins/home/public/application/components/add_data/add_data.tsx b/src/plugins/home/public/application/components/add_data/add_data.tsx
index 97ba28a04a07e..50d6079dd8df3 100644
--- a/src/plugins/home/public/application/components/add_data/add_data.tsx
+++ b/src/plugins/home/public/application/components/add_data/add_data.tsx
@@ -22,8 +22,6 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { METRIC_TYPE } from '@kbn/analytics';
import { ApplicationStart } from 'kibana/public';
import { createAppNavigationHandler } from '../app_navigation_handler';
-// @ts-expect-error untyped component
-import { Synopsis } from '../synopsis';
import { getServices } from '../../kibana_services';
import { RedirectAppLinks } from '../../../../../kibana_react/public';
@@ -35,87 +33,91 @@ interface Props {
export const AddData: FC = ({ addBasePath, application, isDarkMode }) => {
const { trackUiMetric } = getServices();
+ const canAccessIntegrations = application.capabilities.navLinks.integrations;
+ if (canAccessIntegrations) {
+ return (
+ <>
+
+
+
+
+
+
+
+
- return (
- <>
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
-
-
-
-
-
+
-
+
+
+
+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
+ {
+ trackUiMetric(METRIC_TYPE.CLICK, 'home_tutorial_directory');
+ createAppNavigationHandler('/app/integrations/browse')(event);
+ }}
+ >
+
+
+
+
-
-
-
- {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
- {
- trackUiMetric(METRIC_TYPE.CLICK, 'home_tutorial_directory');
- createAppNavigationHandler('/app/home#/tutorial_directory')(event);
- }}
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
- >
- );
+
+ >
+ );
+ } else {
+ return null;
+ }
};
diff --git a/src/plugins/home/public/application/components/sample_data/index.tsx b/src/plugins/home/public/application/components/sample_data/index.tsx
index d6b9328f57e9b..b65fbb5d002b0 100644
--- a/src/plugins/home/public/application/components/sample_data/index.tsx
+++ b/src/plugins/home/public/application/components/sample_data/index.tsx
@@ -40,7 +40,7 @@ export function SampleDataCard({ urlBasePath, onDecline, onConfirm }: Props) {
image={cardGraphicURL}
textAlign="left"
title={
-
+
}
description={
-
+
{
- const notices = getServices().tutorialService.getDirectoryNotices();
- return notices.length ? (
-
- {notices.map((DirectoryNotice, index) => (
-
-
-
- ))}
-
- ) : null;
- };
-
renderHeaderLinks = () => {
const headerLinks = getServices().tutorialService.getDirectoryHeaderLinks();
return headerLinks.length ? (
@@ -245,7 +203,6 @@ class TutorialDirectoryUi extends React.Component {
render() {
const headerLinks = this.renderHeaderLinks();
const tabs = this.getTabs();
- const notices = this.renderNotices();
return (
+
),
tabs,
rightSideItems: headerLinks ? [headerLinks] : [],
}}
>
- {notices && (
- <>
- {notices}
-
- >
- )}
{this.renderTabContent()}
);
diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx
index ca7e6874c75c2..03dff22c7b33f 100644
--- a/src/plugins/home/public/application/components/welcome.tsx
+++ b/src/plugins/home/public/application/components/welcome.tsx
@@ -48,8 +48,7 @@ export class Welcome extends React.Component {
};
private redirecToAddData() {
- const path = this.services.addBasePath('#/tutorial_directory');
- window.location.href = path;
+ this.services.application.navigateToApp('integrations', { path: '/browse' });
}
private onSampleDataDecline = () => {
diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts
index dd02bf65dd8b0..7abaf5d19f008 100644
--- a/src/plugins/home/public/index.ts
+++ b/src/plugins/home/public/index.ts
@@ -23,7 +23,6 @@ export type {
FeatureCatalogueSolution,
Environment,
TutorialVariables,
- TutorialDirectoryNoticeComponent,
TutorialDirectoryHeaderLinkComponent,
TutorialModuleNoticeComponent,
} from './services';
diff --git a/src/plugins/home/public/services/index.ts b/src/plugins/home/public/services/index.ts
index 65913df6310b1..2ee68a9eef0c2 100644
--- a/src/plugins/home/public/services/index.ts
+++ b/src/plugins/home/public/services/index.ts
@@ -22,7 +22,6 @@ export { TutorialService } from './tutorials';
export type {
TutorialVariables,
TutorialServiceSetup,
- TutorialDirectoryNoticeComponent,
TutorialDirectoryHeaderLinkComponent,
TutorialModuleNoticeComponent,
} from './tutorials';
diff --git a/src/plugins/home/public/services/tutorials/index.ts b/src/plugins/home/public/services/tutorials/index.ts
index 8de12c31249d8..e007a5ea4d552 100644
--- a/src/plugins/home/public/services/tutorials/index.ts
+++ b/src/plugins/home/public/services/tutorials/index.ts
@@ -11,7 +11,6 @@ export { TutorialService } from './tutorial_service';
export type {
TutorialVariables,
TutorialServiceSetup,
- TutorialDirectoryNoticeComponent,
TutorialDirectoryHeaderLinkComponent,
TutorialModuleNoticeComponent,
} from './tutorial_service';
diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts
index 0c109d61912ca..ab38a32a1a5b3 100644
--- a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts
+++ b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts
@@ -25,7 +25,6 @@ const createMock = (): jest.Mocked> => {
const service = {
setup: jest.fn(),
getVariables: jest.fn(() => ({})),
- getDirectoryNotices: jest.fn(() => []),
getDirectoryHeaderLinks: jest.fn(() => []),
getModuleNotices: jest.fn(() => []),
getCustomStatusCheck: jest.fn(),
diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx
index a88cf526e3716..b90165aafb45f 100644
--- a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx
+++ b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx
@@ -27,22 +27,6 @@ describe('TutorialService', () => {
}).toThrow();
});
- test('allows multiple register directory notice calls', () => {
- const setup = new TutorialService().setup();
- expect(() => {
- setup.registerDirectoryNotice('abc', () => );
- setup.registerDirectoryNotice('def', () => );
- }).not.toThrow();
- });
-
- test('throws when same directory notice is registered twice', () => {
- const setup = new TutorialService().setup();
- expect(() => {
- setup.registerDirectoryNotice('abc', () => );
- setup.registerDirectoryNotice('abc', () => );
- }).toThrow();
- });
-
test('allows multiple register directory header link calls', () => {
const setup = new TutorialService().setup();
expect(() => {
@@ -91,22 +75,6 @@ describe('TutorialService', () => {
});
});
- describe('getDirectoryNotices', () => {
- test('returns empty array', () => {
- const service = new TutorialService();
- expect(service.getDirectoryNotices()).toEqual([]);
- });
-
- test('returns last state of register calls', () => {
- const service = new TutorialService();
- const setup = service.setup();
- const notices = [() => , () => ];
- setup.registerDirectoryNotice('abc', notices[0]);
- setup.registerDirectoryNotice('def', notices[1]);
- expect(service.getDirectoryNotices()).toEqual(notices);
- });
- });
-
describe('getDirectoryHeaderLinks', () => {
test('returns empty array', () => {
const service = new TutorialService();
diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.ts b/src/plugins/home/public/services/tutorials/tutorial_service.ts
index 839b0702a499e..81b6bbe72e3e9 100644
--- a/src/plugins/home/public/services/tutorials/tutorial_service.ts
+++ b/src/plugins/home/public/services/tutorials/tutorial_service.ts
@@ -11,9 +11,6 @@ import React from 'react';
/** @public */
export type TutorialVariables = Partial>;
-/** @public */
-export type TutorialDirectoryNoticeComponent = React.FC;
-
/** @public */
export type TutorialDirectoryHeaderLinkComponent = React.FC;
@@ -27,7 +24,6 @@ type CustomComponent = () => Promise;
export class TutorialService {
private tutorialVariables: TutorialVariables = {};
- private tutorialDirectoryNotices: { [key: string]: TutorialDirectoryNoticeComponent } = {};
private tutorialDirectoryHeaderLinks: {
[key: string]: TutorialDirectoryHeaderLinkComponent;
} = {};
@@ -47,16 +43,6 @@ export class TutorialService {
this.tutorialVariables[key] = value;
},
- /**
- * Registers a component that will be rendered at the top of tutorial directory page.
- */
- registerDirectoryNotice: (id: string, component: TutorialDirectoryNoticeComponent) => {
- if (this.tutorialDirectoryNotices[id]) {
- throw new Error(`directory notice ${id} already set`);
- }
- this.tutorialDirectoryNotices[id] = component;
- },
-
/**
* Registers a component that will be rendered next to tutorial directory title/header area.
*/
@@ -94,10 +80,6 @@ export class TutorialService {
return this.tutorialVariables;
}
- public getDirectoryNotices() {
- return Object.values(this.tutorialDirectoryNotices);
- }
-
public getDirectoryHeaderLinks() {
return Object.values(this.tutorialDirectoryHeaderLinks);
}
diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx
index 1331eb9b7c4ac..d00f9e2368e21 100644
--- a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx
+++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx
@@ -91,7 +91,7 @@ export const EmptyIndexListPrompt = ({
{
- navigateToApp('home', { path: '#/tutorial_directory' });
+ navigateToApp('home', { path: '/app/integrations/browse' });
closeFlyout();
}}
icon={}
diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap
index 6da2f95fa394d..babcab15a4974 100644
--- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap
+++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap
@@ -226,10 +226,7 @@ exports[`Overview render 1`] = `
[MockFunction] {
"calls": Array [
Array [
- "/app/home#/tutorial_directory",
- ],
- Array [
- "home#/tutorial_directory",
+ "/app/integrations/browse",
],
Array [
"kibana_landing_page",
@@ -259,11 +256,7 @@ exports[`Overview render 1`] = `
"results": Array [
Object {
"type": "return",
- "value": "/app/home#/tutorial_directory",
- },
- Object {
- "type": "return",
- "value": "home#/tutorial_directory",
+ "value": "/app/integrations/browse",
},
Object {
"type": "return",
@@ -533,10 +526,7 @@ exports[`Overview without features 1`] = `
[MockFunction] {
"calls": Array [
Array [
- "/app/home#/tutorial_directory",
- ],
- Array [
- "home#/tutorial_directory",
+ "/app/integrations/browse",
],
Array [
"kibana_landing_page",
@@ -563,16 +553,10 @@ exports[`Overview without features 1`] = `
"/plugins/kibanaReact/assets/solutions_solution_4.svg",
],
Array [
- "/app/home#/tutorial_directory",
+ "/app/integrations/browse",
],
Array [
- "home#/tutorial_directory",
- ],
- Array [
- "/app/home#/tutorial_directory",
- ],
- Array [
- "home#/tutorial_directory",
+ "/app/integrations/browse",
],
Array [
"kibana_landing_page",
@@ -602,11 +586,7 @@ exports[`Overview without features 1`] = `
"results": Array [
Object {
"type": "return",
- "value": "/app/home#/tutorial_directory",
- },
- Object {
- "type": "return",
- "value": "home#/tutorial_directory",
+ "value": "/app/integrations/browse",
},
Object {
"type": "return",
@@ -642,19 +622,11 @@ exports[`Overview without features 1`] = `
},
Object {
"type": "return",
- "value": "/app/home#/tutorial_directory",
- },
- Object {
- "type": "return",
- "value": "home#/tutorial_directory",
- },
- Object {
- "type": "return",
- "value": "/app/home#/tutorial_directory",
+ "value": "/app/integrations/browse",
},
Object {
"type": "return",
- "value": "home#/tutorial_directory",
+ "value": "/app/integrations/browse",
},
Object {
"type": "return",
@@ -801,10 +773,7 @@ exports[`Overview without solutions 1`] = `
[MockFunction] {
"calls": Array [
Array [
- "/app/home#/tutorial_directory",
- ],
- Array [
- "home#/tutorial_directory",
+ "/app/integrations/browse",
],
Array [
"kibana_landing_page",
@@ -831,20 +800,13 @@ exports[`Overview without solutions 1`] = `
"/plugins/kibanaReact/assets/solutions_solution_4.svg",
],
Array [
- "/app/home#/tutorial_directory",
- ],
- Array [
- "home#/tutorial_directory",
+ "/app/integrations/browse",
],
],
"results": Array [
Object {
"type": "return",
- "value": "/app/home#/tutorial_directory",
- },
- Object {
- "type": "return",
- "value": "home#/tutorial_directory",
+ "value": "/app/integrations/browse",
},
Object {
"type": "return",
@@ -880,11 +842,7 @@ exports[`Overview without solutions 1`] = `
},
Object {
"type": "return",
- "value": "/app/home#/tutorial_directory",
- },
- Object {
- "type": "return",
- "value": "home#/tutorial_directory",
+ "value": "/app/integrations/browse",
},
],
}
@@ -898,10 +856,7 @@ exports[`Overview without solutions 1`] = `
[MockFunction] {
"calls": Array [
Array [
- "/app/home#/tutorial_directory",
- ],
- Array [
- "home#/tutorial_directory",
+ "/app/integrations/browse",
],
Array [
"kibana_landing_page",
@@ -928,20 +883,13 @@ exports[`Overview without solutions 1`] = `
"/plugins/kibanaReact/assets/solutions_solution_4.svg",
],
Array [
- "/app/home#/tutorial_directory",
- ],
- Array [
- "home#/tutorial_directory",
+ "/app/integrations/browse",
],
],
"results": Array [
Object {
"type": "return",
- "value": "/app/home#/tutorial_directory",
- },
- Object {
- "type": "return",
- "value": "home#/tutorial_directory",
+ "value": "/app/integrations/browse",
},
Object {
"type": "return",
@@ -977,11 +925,7 @@ exports[`Overview without solutions 1`] = `
},
Object {
"type": "return",
- "value": "/app/home#/tutorial_directory",
- },
- Object {
- "type": "return",
- "value": "home#/tutorial_directory",
+ "value": "/app/integrations/browse",
},
],
}
@@ -1001,10 +945,7 @@ exports[`Overview without solutions 1`] = `
[MockFunction] {
"calls": Array [
Array [
- "/app/home#/tutorial_directory",
- ],
- Array [
- "home#/tutorial_directory",
+ "/app/integrations/browse",
],
Array [
"kibana_landing_page",
@@ -1031,20 +972,13 @@ exports[`Overview without solutions 1`] = `
"/plugins/kibanaReact/assets/solutions_solution_4.svg",
],
Array [
- "/app/home#/tutorial_directory",
- ],
- Array [
- "home#/tutorial_directory",
+ "/app/integrations/browse",
],
],
"results": Array [
Object {
"type": "return",
- "value": "/app/home#/tutorial_directory",
- },
- Object {
- "type": "return",
- "value": "home#/tutorial_directory",
+ "value": "/app/integrations/browse",
},
Object {
"type": "return",
@@ -1080,11 +1014,7 @@ exports[`Overview without solutions 1`] = `
},
Object {
"type": "return",
- "value": "/app/home#/tutorial_directory",
- },
- Object {
- "type": "return",
- "value": "home#/tutorial_directory",
+ "value": "/app/integrations/browse",
},
],
}
diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx
index 07769e2f3c474..6a0279bd12465 100644
--- a/src/plugins/kibana_overview/public/components/overview/overview.tsx
+++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx
@@ -61,7 +61,7 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) =>
const IS_DARK_THEME = uiSettings.get('theme:darkMode');
// Home does not have a locator implemented, so hard-code it here.
- const addDataHref = addBasePath('/app/home#/tutorial_directory');
+ const addDataHref = addBasePath('/app/integrations/browse');
const devToolsHref = share.url.locators.get('CONSOLE_APP_LOCATOR')?.useUrl({});
const managementHref = share.url.locators
.get('MANAGEMENT_APP_LOCATOR')
@@ -86,8 +86,14 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) =>
}),
logo: 'logoKibana',
actions: {
- beats: {
- href: addBasePath(`home#/tutorial_directory`),
+ elasticAgent: {
+ title: i18n.translate('kibanaOverview.noDataConfig.title', {
+ defaultMessage: 'Add integrations',
+ }),
+ description: i18n.translate('kibanaOverview.noDataConfig.description', {
+ defaultMessage:
+ 'Use Elastic Agent or Beats to collect data and build out Analytics solutions.',
+ }),
},
},
docsLink: docLinks.links.kibana,
diff --git a/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg b/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg
deleted file mode 100644
index 8652d8d921506..0000000000000
--- a/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg b/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg
deleted file mode 100644
index f54786c1b950c..0000000000000
--- a/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap
index d8bc5745ec8e5..8842a3c9f5842 100644
--- a/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap
+++ b/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap
@@ -73,9 +73,9 @@ exports[`NoDataPage render 1`] = `
-
diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap
index 3f72ae5597a98..f66d05140b2e9 100644
--- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap
+++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap
@@ -13,7 +13,36 @@ exports[`ElasticAgentCard props button 1`] = `
href="/app/integrations/browse"
image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
paddingSize="l"
- title="Add Elastic Agent"
+ title={
+
+
+ Add Elastic Agent
+
+
+ }
+/>
+`;
+
+exports[`ElasticAgentCard props category 1`] = `
+
+ Add Elastic Agent
+
+ }
+ href="/app/integrations/browse/custom"
+ image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
+ paddingSize="l"
+ title={
+
+
+ Add Elastic Agent
+
+
+ }
/>
`;
@@ -30,7 +59,13 @@ exports[`ElasticAgentCard props href 1`] = `
href="#"
image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
paddingSize="l"
- title="Add Elastic Agent"
+ title={
+
+
+ Add Elastic Agent
+
+
+ }
/>
`;
@@ -48,7 +83,13 @@ exports[`ElasticAgentCard props recommended 1`] = `
href="/app/integrations/browse"
image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
paddingSize="l"
- title="Add Elastic Agent"
+ title={
+
+
+ Add Elastic Agent
+
+
+ }
/>
`;
@@ -65,6 +106,12 @@ exports[`ElasticAgentCard renders 1`] = `
href="/app/integrations/browse"
image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
paddingSize="l"
- title="Add Elastic Agent"
+ title={
+
+
+ Add Elastic Agent
+
+
+ }
/>
`;
diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap
deleted file mode 100644
index af26f9e93ebac..0000000000000
--- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap
+++ /dev/null
@@ -1,70 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ElasticBeatsCard props button 1`] = `
-
- Button
-
- }
- href="/app/home#/tutorial_directory"
- image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg"
- paddingSize="l"
- title="Add data"
-/>
-`;
-
-exports[`ElasticBeatsCard props href 1`] = `
-
- Button
-
- }
- href="#"
- image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg"
- paddingSize="l"
- title="Add data"
-/>
-`;
-
-exports[`ElasticBeatsCard props recommended 1`] = `
-
- Add data
-
- }
- href="/app/home#/tutorial_directory"
- image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg"
- paddingSize="l"
- title="Add data"
-/>
-`;
-
-exports[`ElasticBeatsCard renders 1`] = `
-
- Add data
-
- }
- href="/app/home#/tutorial_directory"
- image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg"
- paddingSize="l"
- title="Add data"
-/>
-`;
diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx
index 45cc32cae06d6..b971abf06a437 100644
--- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx
+++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx
@@ -14,7 +14,10 @@ jest.mock('../../../context', () => ({
...jest.requireActual('../../../context'),
useKibana: jest.fn().mockReturnValue({
services: {
- http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } },
+ http: {
+ basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) },
+ },
+ application: { capabilities: { navLinks: { integrations: true } } },
uiSettings: { get: jest.fn() },
},
}),
@@ -41,5 +44,10 @@ describe('ElasticAgentCard', () => {
const component = shallow();
expect(component).toMatchSnapshot();
});
+
+ test('category', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+ });
});
});
diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx
index f071bd9fab25a..5a91e568471d1 100644
--- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx
+++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx
@@ -9,7 +9,7 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { CoreStart } from 'kibana/public';
-import { EuiButton, EuiCard } from '@elastic/eui';
+import { EuiButton, EuiCard, EuiTextColor, EuiScreenReaderOnly } from '@elastic/eui';
import { useKibana } from '../../../context';
import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page';
@@ -27,13 +27,40 @@ export const ElasticAgentCard: FunctionComponent = ({
href,
button,
layout,
+ category,
...cardRest
}) => {
const {
- services: { http },
+ services: { http, application },
} = useKibana();
const addBasePath = http.basePath.prepend;
- const basePathUrl = '/plugins/kibanaReact/assets/';
+ const image = addBasePath(`/plugins/kibanaReact/assets/elastic_agent_card.svg`);
+ const canAccessFleet = application.capabilities.navLinks.integrations;
+ const hasCategory = category ? `/${category}` : '';
+
+ if (!canAccessFleet) {
+ return (
+
+ {i18n.translate('kibana-react.noDataPage.elasticAgentCard.noPermission.title', {
+ defaultMessage: `Contact your administrator`,
+ })}
+
+ }
+ description={
+
+ {i18n.translate('kibana-react.noDataPage.elasticAgentCard.noPermission.description', {
+ defaultMessage: `This integration is not yet enabled. Your administrator has the required permissions to turn it on.`,
+ })}
+
+ }
+ isDisabled
+ />
+ );
+ }
const defaultCTAtitle = i18n.translate('kibana-react.noDataPage.elasticAgentCard.title', {
defaultMessage: 'Add Elastic Agent',
@@ -51,12 +78,17 @@ export const ElasticAgentCard: FunctionComponent = ({
return (
+ {defaultCTAtitle}
+
+ }
description={i18n.translate('kibana-react.noDataPage.elasticAgentCard.description', {
defaultMessage: `Use Elastic Agent for a simple, unified way to collect data from your machines.`,
})}
- image={addBasePath(`${basePathUrl}elastic_agent_card.svg`)}
betaBadgeLabel={recommended ? NO_DATA_RECOMMENDED : undefined}
footer={footer}
layout={layout as 'vertical' | undefined}
diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx
deleted file mode 100644
index 6ea41bf6b3e1f..0000000000000
--- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx
+++ /dev/null
@@ -1,45 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import { shallow } from 'enzyme';
-import React from 'react';
-import { ElasticBeatsCard } from './elastic_beats_card';
-
-jest.mock('../../../context', () => ({
- ...jest.requireActual('../../../context'),
- useKibana: jest.fn().mockReturnValue({
- services: {
- http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } },
- uiSettings: { get: jest.fn() },
- },
- }),
-}));
-
-describe('ElasticBeatsCard', () => {
- test('renders', () => {
- const component = shallow();
- expect(component).toMatchSnapshot();
- });
-
- describe('props', () => {
- test('recommended', () => {
- const component = shallow();
- expect(component).toMatchSnapshot();
- });
-
- test('button', () => {
- const component = shallow();
- expect(component).toMatchSnapshot();
- });
-
- test('href', () => {
- const component = shallow();
- expect(component).toMatchSnapshot();
- });
- });
-});
diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx
deleted file mode 100644
index 0372d12096489..0000000000000
--- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 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 or the Server
- * Side Public License, v 1.
- */
-
-import React, { FunctionComponent } from 'react';
-import { i18n } from '@kbn/i18n';
-import { CoreStart } from 'kibana/public';
-import { EuiButton, EuiCard } from '@elastic/eui';
-import { useKibana } from '../../../context';
-import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page';
-
-export type ElasticBeatsCardProps = NoDataPageActions & {
- solution: string;
-};
-
-export const ElasticBeatsCard: FunctionComponent = ({
- recommended,
- title,
- button,
- href,
- solution, // unused for now
- layout,
- ...cardRest
-}) => {
- const {
- services: { http, uiSettings },
- } = useKibana();
- const addBasePath = http.basePath.prepend;
- const basePathUrl = '/plugins/kibanaReact/assets/';
- const IS_DARK_THEME = uiSettings.get('theme:darkMode');
-
- const defaultCTAtitle = i18n.translate('kibana-react.noDataPage.elasticBeatsCard.title', {
- defaultMessage: 'Add data',
- });
-
- const footer =
- typeof button !== 'string' && typeof button !== 'undefined' ? (
- button
- ) : (
- // The href and/or onClick are attached to the whole Card, so the button is just for show.
- // Do not add the behavior here too or else it will propogate through
- {button || title || defaultCTAtitle}
- );
-
- return (
-
- );
-};
diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts
index 3744239d9a472..e05d4d9675ca9 100644
--- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts
+++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts
@@ -7,5 +7,4 @@
*/
export * from './elastic_agent_card';
-export * from './elastic_beats_card';
export * from './no_data_card';
diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx
index 56eb0f34617d6..b2d9ef6ca5008 100644
--- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx
+++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx
@@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { KibanaPageTemplateProps } from '../page_template';
-import { ElasticAgentCard, ElasticBeatsCard, NoDataCard } from './no_data_card';
+import { ElasticAgentCard, NoDataCard } from './no_data_card';
import { KibanaPageTemplateSolutionNavAvatar } from '../solution_nav';
export const NO_DATA_PAGE_MAX_WIDTH = 950;
@@ -55,6 +55,10 @@ export type NoDataPageActions = Partial & {
* Remapping `onClick` to any element
*/
onClick?: MouseEventHandler;
+ /**
+ * Category to auto-select within Fleet
+ */
+ category?: string;
};
export type NoDataPageActionsProps = Record;
@@ -107,18 +111,12 @@ export const NoDataPage: FunctionComponent = ({
const actionsKeys = Object.keys(sortedData);
const renderActions = useMemo(() => {
return Object.values(sortedData).map((action, i) => {
- if (actionsKeys[i] === 'elasticAgent') {
+ if (actionsKeys[i] === 'elasticAgent' || actionsKeys[i] === 'beats') {
return (
);
- } else if (actionsKeys[i] === 'beats') {
- return (
-
-
-
- );
} else {
return (
),
discussForumLink: (
-
+
import('./tutorial_directory_notice'));
-export const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = () => (
- }>
-
-
-);
-
const TutorialDirectoryHeaderLinkLazy = React.lazy(
() => import('./tutorial_directory_header_link')
);
diff --git a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx
index 074a1c40bdb19..18fdd875c7379 100644
--- a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx
+++ b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { memo, useState, useEffect } from 'react';
+import React, { memo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButtonEmpty } from '@elastic/eui';
import type { TutorialDirectoryHeaderLinkComponent } from 'src/plugins/home/public';
@@ -13,25 +13,15 @@ import type { TutorialDirectoryHeaderLinkComponent } from 'src/plugins/home/publ
import { RedirectAppLinks } from '../../../../../../src/plugins/kibana_react/public';
import { useLink, useCapabilities, useStartServices } from '../../hooks';
-import { tutorialDirectoryNoticeState$ } from './tutorial_directory_notice';
-
const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo(() => {
const { getHref } = useLink();
const { application } = useStartServices();
const { show: hasIngestManager } = useCapabilities();
- const [noticeState, setNoticeState] = useState({
+ const [noticeState] = useState({
settingsDataLoaded: false,
- hasSeenNotice: false,
});
- useEffect(() => {
- const subscription = tutorialDirectoryNoticeState$.subscribe((value) => setNoticeState(value));
- return () => {
- subscription.unsubscribe();
- };
- }, []);
-
- return hasIngestManager && noticeState.settingsDataLoaded && noticeState.hasSeenNotice ? (
+ return hasIngestManager && noticeState.settingsDataLoaded ? (
{
- const { getHref } = useLink();
- const { application } = useStartServices();
- const { show: hasIngestManager } = useCapabilities();
- const { data: settingsData, isLoading } = useGetSettings();
- const [dismissedNotice, setDismissedNotice] = useState(false);
-
- const dismissNotice = useCallback(async () => {
- setDismissedNotice(true);
- await sendPutSettings({
- has_seen_add_data_notice: true,
- });
- }, []);
-
- useEffect(() => {
- tutorialDirectoryNoticeState$.next({
- settingsDataLoaded: !isLoading,
- hasSeenNotice: Boolean(dismissedNotice || settingsData?.item?.has_seen_add_data_notice),
- });
- }, [isLoading, settingsData, dismissedNotice]);
-
- const hasSeenNotice =
- isLoading || settingsData?.item?.has_seen_add_data_notice || dismissedNotice;
-
- return hasIngestManager && !hasSeenNotice ? (
- <>
-
-
-
- ),
- }}
- />
- }
- >
-
-
-
-
- ),
- }}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- dismissNotice();
- }}
- >
-
-
-
-
-
-
-
- >
- ) : null;
-});
-
-// Needed for React.lazy
-// eslint-disable-next-line import/no-default-export
-export default TutorialDirectoryNotice;
diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts
index e1f263b0763e8..4a2a6900cc78c 100644
--- a/x-pack/plugins/fleet/public/plugin.ts
+++ b/x-pack/plugins/fleet/public/plugin.ts
@@ -44,11 +44,7 @@ import { CUSTOM_LOGS_INTEGRATION_NAME, INTEGRATIONS_BASE_PATH } from './constant
import { licenseService } from './hooks';
import { setHttpClient } from './hooks/use_request';
import { createPackageSearchProvider } from './search_provider';
-import {
- TutorialDirectoryNotice,
- TutorialDirectoryHeaderLink,
- TutorialModuleNotice,
-} from './components/home_integration';
+import { TutorialDirectoryHeaderLink, TutorialModuleNotice } from './components/home_integration';
import { createExtensionRegistrationCallback } from './services/ui_extensions';
import type { UIExtensionRegistrationCallback, UIExtensionsStorage } from './types';
import { LazyCustomLogsAssetsExtension } from './lazy_custom_logs_assets_extension';
@@ -197,7 +193,6 @@ export class FleetPlugin implements Plugin {
diff --git a/x-pack/plugins/infra/public/pages/logs/page_template.tsx b/x-pack/plugins/infra/public/pages/logs/page_template.tsx
index 7ee60ab84bf25..6de13b495f0ba 100644
--- a/x-pack/plugins/infra/public/pages/logs/page_template.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/page_template.tsx
@@ -44,13 +44,13 @@ export const LogsPageTemplate: React.FC = ({
actions: {
beats: {
title: i18n.translate('xpack.infra.logs.noDataConfig.beatsCard.title', {
- defaultMessage: 'Add logs with Beats',
+ defaultMessage: 'Add a logging integration',
}),
description: i18n.translate('xpack.infra.logs.noDataConfig.beatsCard.description', {
defaultMessage:
- 'Use Beats to send logs to Elasticsearch. We make it easy with modules for many popular systems and apps.',
+ 'Use the Elastic Agent or Beats to send logs to Elasticsearch. We make it easy with integrations for many popular systems and apps.',
}),
- href: basePath + `/app/home#/tutorial_directory/logging`,
+ href: basePath + `/app/integrations/browse`,
},
},
docsLink: docLinks.links.observability.guide,
diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx
index bc3bc22f3f1b2..2259a8d3528af 100644
--- a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx
@@ -22,8 +22,8 @@ export const LogsPageNoIndicesContent = () => {
const canConfigureSource = application?.capabilities?.logs?.configureSource ? true : false;
const tutorialLinkProps = useLinkProps({
- app: 'home',
- hash: '/tutorial_directory/logging',
+ app: 'integrations',
+ hash: '/browse',
});
return (
diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx
index ae375dc504e7a..1a79cd996087d 100644
--- a/x-pack/plugins/infra/public/pages/metrics/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx
@@ -93,9 +93,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx
index 2a436eac30b2c..17e6382ce65cc 100644
--- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx
@@ -18,8 +18,8 @@ interface InvalidNodeErrorProps {
export const InvalidNodeError: React.FunctionComponent = ({ nodeName }) => {
const tutorialLinkProps = useLinkProps({
- app: 'home',
- hash: '/tutorial_directory/metrics',
+ app: 'integrations',
+ hash: '/browse',
});
return (
diff --git a/x-pack/plugins/infra/public/pages/metrics/page_template.tsx b/x-pack/plugins/infra/public/pages/metrics/page_template.tsx
index 41ea12c280841..4da671283644d 100644
--- a/x-pack/plugins/infra/public/pages/metrics/page_template.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/page_template.tsx
@@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n';
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
import type { LazyObservabilityPageTemplateProps } from '../../../../observability/public';
import { KibanaPageTemplateProps } from '../../../../../../src/plugins/kibana_react/public';
-import { useLinkProps } from '../../hooks/use_link_props';
interface MetricsPageTemplateProps extends LazyObservabilityPageTemplateProps {
hasData?: boolean;
@@ -30,11 +29,6 @@ export const MetricsPageTemplate: React.FC = ({
},
} = useKibanaContextForPlugin();
- const tutorialLinkProps = useLinkProps({
- app: 'home',
- hash: '/tutorial_directory/metrics',
- });
-
const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = hasData
? undefined
: {
@@ -44,13 +38,12 @@ export const MetricsPageTemplate: React.FC = ({
actions: {
beats: {
title: i18n.translate('xpack.infra.metrics.noDataConfig.beatsCard.title', {
- defaultMessage: 'Add metrics with Beats',
+ defaultMessage: 'Add a metrics integration',
}),
description: i18n.translate('xpack.infra.metrics.noDataConfig.beatsCard.description', {
defaultMessage:
'Use Beats to send metrics data to Elasticsearch. We make it easy with modules for many popular systems and apps.',
}),
- ...tutorialLinkProps,
},
},
docsLink: docLinks.links.observability.guide,
diff --git a/x-pack/plugins/observability/public/components/app/header/header_menu.tsx b/x-pack/plugins/observability/public/components/app/header/header_menu.tsx
index 707cb241501fd..0ed01b7d3673e 100644
--- a/x-pack/plugins/observability/public/components/app/header/header_menu.tsx
+++ b/x-pack/plugins/observability/public/components/app/header/header_menu.tsx
@@ -26,7 +26,7 @@ export function ObservabilityHeaderMenu(): React.ReactElement | null {
{addDataLinkText}
diff --git a/x-pack/plugins/observability/public/utils/no_data_config.ts b/x-pack/plugins/observability/public/utils/no_data_config.ts
index 1e16fb145bdce..2c87b1434a0b4 100644
--- a/x-pack/plugins/observability/public/utils/no_data_config.ts
+++ b/x-pack/plugins/observability/public/utils/no_data_config.ts
@@ -24,12 +24,15 @@ export function getNoDataConfig({
defaultMessage: 'Observability',
}),
actions: {
- beats: {
+ elasticAgent: {
+ title: i18n.translate('xpack.observability.noDataConfig.beatsCard.title', {
+ defaultMessage: 'Add integrations',
+ }),
description: i18n.translate('xpack.observability.noDataConfig.beatsCard.description', {
defaultMessage:
'Use Beats and APM agents to send observability data to Elasticsearch. We make it easy with support for many popular systems, apps, and languages.',
}),
- href: basePath.prepend(`/app/home#/tutorial_directory/logging`),
+ href: basePath.prepend(`/app/integrations`),
},
},
docsLink,
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 5c41e92661e58..5a7e19e2cdd05 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -16,7 +16,7 @@ export const APP_NAME = 'Security';
export const APP_ICON = 'securityAnalyticsApp';
export const APP_ICON_SOLUTION = 'logoSecurity';
export const APP_PATH = `/app/security`;
-export const ADD_DATA_PATH = `/app/home#/tutorial_directory/security`;
+export const ADD_DATA_PATH = `/app/integrations/browse/security`;
export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern';
export const DEFAULT_DATE_FORMAT = 'dateFormat';
export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz';
diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx
index 5fa2725f9ee6f..61e9e66f1bb87 100644
--- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx
@@ -45,9 +45,10 @@ describe('OverviewEmpty', () => {
expect(wrapper.find('[data-test-subj="empty-page"]').prop('noDataConfig')).toEqual({
actions: {
elasticAgent: {
+ category: 'security',
description:
- 'Use Elastic Agent to collect security events and protect your endpoints from threats. Manage your agents in Fleet and add integrations with a single click.',
- href: '/app/integrations/browse/security',
+ 'Use Elastic Agent to collect security events and protect your endpoints from threats.',
+ title: 'Add a Security integration',
},
},
docsLink: 'https://www.elastic.co/guide/en/security/mocked-test-branch/index.html',
@@ -68,8 +69,11 @@ describe('OverviewEmpty', () => {
it('render with correct actions ', () => {
expect(wrapper.find('[data-test-subj="empty-page"]').prop('noDataConfig')).toEqual({
actions: {
- beats: {
- href: '/app/home#/tutorial_directory/security',
+ elasticAgent: {
+ category: 'security',
+ description:
+ 'Use Elastic Agent to collect security events and protect your endpoints from threats.',
+ title: 'Add a Security integration',
},
},
docsLink: 'https://www.elastic.co/guide/en/security/mocked-test-branch/index.html',
diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx
index bc76333943191..9b20c079002e6 100644
--- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx
@@ -5,13 +5,10 @@
* 2.0.
*/
-import React, { useMemo } from 'react';
+import React from 'react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../../common/lib/kibana';
-import { ADD_DATA_PATH } from '../../../../common/constants';
-import { pagePathGetters } from '../../../../../fleet/public';
import { SOLUTION_NAME } from '../../../../public/common/translations';
-import { useUserPrivileges } from '../../../common/components/user_privileges';
import {
KibanaPageTemplate,
@@ -19,42 +16,27 @@ import {
} from '../../../../../../../src/plugins/kibana_react/public';
const OverviewEmptyComponent: React.FC = () => {
- const { http, docLinks } = useKibana().services;
- const basePath = http.basePath.get();
- const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet;
- const integrationsPathComponents = pagePathGetters.integrations_all({ category: 'security' });
-
- const agentAction: NoDataPageActionsProps = useMemo(
- () => ({
- elasticAgent: {
- href: `${basePath}${integrationsPathComponents[0]}${integrationsPathComponents[1]}`,
- description: i18n.translate(
- 'xpack.securitySolution.pages.emptyPage.beatsCard.description',
- {
- defaultMessage:
- 'Use Elastic Agent to collect security events and protect your endpoints from threats. Manage your agents in Fleet and add integrations with a single click.',
- }
- ),
- },
- }),
- [basePath, integrationsPathComponents]
- );
-
- const beatsAction: NoDataPageActionsProps = useMemo(
- () => ({
- beats: {
- href: `${basePath}${ADD_DATA_PATH}`,
- },
- }),
- [basePath]
- );
+ const { docLinks } = useKibana().services;
+
+ const agentAction: NoDataPageActionsProps = {
+ elasticAgent: {
+ category: 'security',
+ title: i18n.translate('xpack.securitySolution.pages.emptyPage.beatsCard.title', {
+ defaultMessage: 'Add a Security integration',
+ }),
+ description: i18n.translate('xpack.securitySolution.pages.emptyPage.beatsCard.description', {
+ defaultMessage:
+ 'Use Elastic Agent to collect security events and protect your endpoints from threats.',
+ }),
+ },
+ };
return (
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index f909a03909e3f..95f6909b12f6c 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -3014,11 +3014,7 @@
"home.tutorial.savedObject.unableToAddErrorMessage": "{savedObjectsLength} 件中 {errorsLength} 件の kibana オブジェクトが追加できません。エラー:{errorMessage}",
"home.tutorial.selectionLegend": "デプロイタイプ",
"home.tutorial.selfManagedButtonLabel": "自己管理",
- "home.tutorial.tabs.allTitle": "すべて",
- "home.tutorial.tabs.loggingTitle": "ログ",
- "home.tutorial.tabs.metricsTitle": "メトリック",
"home.tutorial.tabs.sampleDataTitle": "サンプルデータ",
- "home.tutorial.tabs.securitySolutionTitle": "セキュリティ",
"home.tutorial.unexpectedStatusCheckStateErrorDescription": "予期せぬステータス確認ステータス {statusCheckState}",
"home.tutorial.unhandledInstructionTypeErrorDescription": "予期せぬ指示タイプ {visibleInstructions}",
"home.tutorialDirectory.featureCatalogueDescription": "一般的なアプリやサービスからデータを取り込みます。",
@@ -4209,8 +4205,6 @@
"kibana-react.noDataPage.cantDecide.link": "詳細については、ドキュメントをご確認ください。",
"kibana-react.noDataPage.elasticAgentCard.description": "Elasticエージェントを使用すると、シンプルで統一された方法でコンピューターからデータを収集するできます。",
"kibana-react.noDataPage.elasticAgentCard.title": "Elasticエージェントの追加",
- "kibana-react.noDataPage.elasticBeatsCard.description": "Beatsを使用して、さまざまなシステムのデータをElasticsearchに追加します。",
- "kibana-react.noDataPage.elasticBeatsCard.title": "データの追加",
"kibana-react.noDataPage.intro": "データを追加して開始するか、{solution}については{link}をご覧ください。",
"kibana-react.noDataPage.intro.link": "詳細",
"kibana-react.noDataPage.noDataPage.recommended": "推奨",
@@ -11205,12 +11199,7 @@
"xpack.fleet.fleetServerUpgradeModal.modalTitle": "エージェントをFleetサーバーに登録",
"xpack.fleet.fleetServerUpgradeModal.onPremDescriptionMessage": "Fleetサーバーが使用できます。スケーラビリティとセキュリティが改善されています。{existingAgentsMessage} Fleetを使用し続けるには、Fleetサーバーと新しいバージョンのElasticエージェントを各ホストにインストールする必要があります。詳細については、{link}をご覧ください。",
"xpack.fleet.genericActionsMenuText": "開く",
- "xpack.fleet.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "メッセージを消去",
"xpack.fleet.homeIntegration.tutorialDirectory.fleetAppButtonText": "統合を試す",
- "xpack.fleet.homeIntegration.tutorialDirectory.noticeText": "Elasticエージェント統合では、シンプルかつ統合された方法で、ログ、メトリック、他の種類のデータの監視をホストに追加することができます。複数のBeatsをインストールする必要はありません。このため、インフラストラクチャ全体でのポリシーのデプロイが簡単で高速になりました。詳細については、{blogPostLink}をお読みください。",
- "xpack.fleet.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "発表ブログ投稿",
- "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix} Elasticエージェント統合",
- "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle.newPrefix": "一般公開へ:",
"xpack.fleet.homeIntegration.tutorialModule.noticeText": "{notePrefix}このモジュールの新しいバージョンは{availableAsIntegrationLink}です。統合と新しいElasticエージェントの詳細については、{blogPostLink}をお読みください。",
"xpack.fleet.homeIntegration.tutorialModule.noticeText.blogPostLink": "発表ブログ投稿",
"xpack.fleet.homeIntegration.tutorialModule.noticeText.integrationLink": "Elasticエージェント統合として提供",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 4de407cf8e464..80dad5a2c0c8b 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -3043,11 +3043,7 @@
"home.tutorial.savedObject.unableToAddErrorMessage": "{savedObjectsLength} 个 kibana 对象中有 {errorsLength} 个无法添加,错误:{errorMessage}",
"home.tutorial.selectionLegend": "部署类型",
"home.tutorial.selfManagedButtonLabel": "自管型",
- "home.tutorial.tabs.allTitle": "全部",
- "home.tutorial.tabs.loggingTitle": "日志",
- "home.tutorial.tabs.metricsTitle": "指标",
"home.tutorial.tabs.sampleDataTitle": "样例数据",
- "home.tutorial.tabs.securitySolutionTitle": "安全",
"home.tutorial.unexpectedStatusCheckStateErrorDescription": "意外的状态检查状态 {statusCheckState}",
"home.tutorial.unhandledInstructionTypeErrorDescription": "未处理的指令类型 {visibleInstructions}",
"home.tutorialDirectory.featureCatalogueDescription": "从热门应用和服务中采集数据。",
@@ -4249,8 +4245,6 @@
"kibana-react.noDataPage.cantDecide.link": "请参阅我们的文档以了解更多信息。",
"kibana-react.noDataPage.elasticAgentCard.description": "使用 Elastic 代理以简单统一的方式从您的计算机中收集数据。",
"kibana-react.noDataPage.elasticAgentCard.title": "添加 Elastic 代理",
- "kibana-react.noDataPage.elasticBeatsCard.description": "使用 Beats 将各种系统的数据添加到 Elasticsearch。",
- "kibana-react.noDataPage.elasticBeatsCard.title": "添加数据",
"kibana-react.noDataPage.intro": "添加您的数据以开始,或{link}{solution}。",
"kibana-react.noDataPage.intro.link": "了解详情",
"kibana-react.noDataPage.noDataPage.recommended": "推荐",
@@ -11321,12 +11315,7 @@
"xpack.fleet.fleetServerUpgradeModal.modalTitle": "将代理注册到 Fleet 服务器",
"xpack.fleet.fleetServerUpgradeModal.onPremDescriptionMessage": "Fleet 服务器现在可用且提供改善的可扩展性和安全性。{existingAgentsMessage}要继续使用 Fleet,必须在各个主机上安装 Fleet 服务器和新版 Elastic 代理。详细了解我们的 {link}。",
"xpack.fleet.genericActionsMenuText": "打开",
- "xpack.fleet.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "关闭消息",
"xpack.fleet.homeIntegration.tutorialDirectory.fleetAppButtonText": "试用集成",
- "xpack.fleet.homeIntegration.tutorialDirectory.noticeText": "通过 Elastic 代理集成,可以简单统一的方式将日志、指标和其他类型数据的监测添加到主机。不再需要安装多个 Beats,这样将策略部署到整个基础架构更容易也更快速。有关更多信息,请阅读我们的{blogPostLink}。",
- "xpack.fleet.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "公告博客",
- "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix}Elastic 代理集成",
- "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle.newPrefix": "已正式发布:",
"xpack.fleet.homeIntegration.tutorialModule.noticeText": "{notePrefix}此模块的较新版本{availableAsIntegrationLink}。要详细了解集成和新 Elastic 代理,请阅读我们的{blogPostLink}。",
"xpack.fleet.homeIntegration.tutorialModule.noticeText.blogPostLink": "公告博客",
"xpack.fleet.homeIntegration.tutorialModule.noticeText.integrationLink": "将作为 Elastic 代理集成来提供",
diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts
index a7158d9579b60..61297859c29f8 100644
--- a/x-pack/test/accessibility/apps/home.ts
+++ b/x-pack/test/accessibility/apps/home.ts
@@ -64,33 +64,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await a11y.testAppSnapshot();
});
- it('Add data page meets a11y requirements ', async () => {
- await home.clickGoHome();
- await testSubjects.click('homeAddData');
- await a11y.testAppSnapshot();
- });
-
- it('Sample data page meets a11y requirements ', async () => {
- await testSubjects.click('homeTab-sampleData');
- await a11y.testAppSnapshot();
- });
-
- it('click on Add logs panel to open all log examples page meets a11y requirements ', async () => {
- await testSubjects.click('sampleDataSetCardlogs');
- await a11y.testAppSnapshot();
- });
-
- it('click on ActiveMQ logs panel to open tutorial meets a11y requirements', async () => {
- await testSubjects.click('homeTab-all');
- await testSubjects.click('homeSynopsisLinkactivemqlogs');
- await a11y.testAppSnapshot();
- });
-
- it('click on cloud tutorial meets a11y requirements', async () => {
- await testSubjects.click('onCloudTutorial');
- await a11y.testAppSnapshot();
- });
-
it('passes with searchbox open', async () => {
await testSubjects.click('nav-search-popover');
await a11y.testAppSnapshot();
From 828437621762ec327a5bf8f2b53927204d21776b Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 19 Oct 2021 07:43:48 -0400
Subject: [PATCH 17/18] [RAC] [Metrics UI] Include group name in the reason
message (#115171) (#115523)
* [RAC] [Metrics UI] Include group name in the reason message
* remove console log
* fix i18n errors
* fix more i18n errors
* fix i18n & check errors and move group to the end of the reason text
* add empty lines at the end of translation files
* fix more i18n tests
* try to remove manually added translations
* Revert "try to remove manually added translations"
This reverts commit 6949af2f70aff46b088bab5c942497ad46081d90.
* apply i18n_check fix and reorder values in the formatted reason
* log threshold reformat reason message and move group info at the end
Co-authored-by: mgiota
---
.../server/lib/alerting/common/messages.ts | 18 ++++++----
.../inventory_metric_threshold_executor.ts | 35 +++++++++++--------
.../log_threshold/reason_formatters.ts | 4 +--
.../metric_threshold_executor.ts | 9 ++---
.../translations/translations/ja-JP.json | 3 --
.../translations/translations/zh-CN.json | 3 --
6 files changed, 40 insertions(+), 32 deletions(-)
diff --git a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
index 084043f357bb1..23c89abf4a7aa 100644
--- a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
@@ -109,15 +109,17 @@ const thresholdToI18n = ([a, b]: Array) => {
};
export const buildFiredAlertReason: (alertResult: {
+ group: string;
metric: string;
comparator: Comparator;
threshold: Array;
currentValue: number | string;
-}) => string = ({ metric, comparator, threshold, currentValue }) =>
+}) => string = ({ group, metric, comparator, threshold, currentValue }) =>
i18n.translate('xpack.infra.metrics.alerting.threshold.firedAlertReason', {
defaultMessage:
- '{metric} is {comparator} a threshold of {threshold} (current value is {currentValue})',
+ '{metric} is {comparator} a threshold of {threshold} (current value is {currentValue}) for {group}',
values: {
+ group,
metric,
comparator: comparatorToI18n(comparator, threshold.map(toNumber), toNumber(currentValue)),
threshold: thresholdToI18n(threshold),
@@ -126,14 +128,15 @@ export const buildFiredAlertReason: (alertResult: {
});
export const buildRecoveredAlertReason: (alertResult: {
+ group: string;
metric: string;
comparator: Comparator;
threshold: Array;
currentValue: number | string;
-}) => string = ({ metric, comparator, threshold, currentValue }) =>
+}) => string = ({ group, metric, comparator, threshold, currentValue }) =>
i18n.translate('xpack.infra.metrics.alerting.threshold.recoveredAlertReason', {
defaultMessage:
- '{metric} is now {comparator} a threshold of {threshold} (current value is {currentValue})',
+ '{metric} is now {comparator} a threshold of {threshold} (current value is {currentValue}) for {group}',
values: {
metric,
comparator: recoveredComparatorToI18n(
@@ -143,19 +146,22 @@ export const buildRecoveredAlertReason: (alertResult: {
),
threshold: thresholdToI18n(threshold),
currentValue,
+ group,
},
});
export const buildNoDataAlertReason: (alertResult: {
+ group: string;
metric: string;
timeSize: number;
timeUnit: string;
-}) => string = ({ metric, timeSize, timeUnit }) =>
+}) => string = ({ group, metric, timeSize, timeUnit }) =>
i18n.translate('xpack.infra.metrics.alerting.threshold.noDataAlertReason', {
- defaultMessage: '{metric} has reported no data over the past {interval}',
+ defaultMessage: '{metric} has reported no data over the past {interval} for {group}',
values: {
metric,
interval: `${timeSize}${timeUnit}`,
+ group,
},
});
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
index 72d9ea9e39def..d8b66b35c703b 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
@@ -111,18 +111,18 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
)
);
const inventoryItems = Object.keys(first(results)!);
- for (const item of inventoryItems) {
+ for (const group of inventoryItems) {
// AND logic; all criteria must be across the threshold
const shouldAlertFire = results.every((result) => {
// Grab the result of the most recent bucket
- return last(result[item].shouldFire);
+ return last(result[group].shouldFire);
});
- const shouldAlertWarn = results.every((result) => last(result[item].shouldWarn));
+ const shouldAlertWarn = results.every((result) => last(result[group].shouldWarn));
// AND logic; because we need to evaluate all criteria, if one of them reports no data then the
// whole alert is in a No Data/Error state
- const isNoData = results.some((result) => last(result[item].isNoData));
- const isError = results.some((result) => result[item].isError);
+ const isNoData = results.some((result) => last(result[group].isNoData));
+ const isError = results.some((result) => result[group].isError);
const nextState = isError
? AlertStates.ERROR
@@ -138,7 +138,8 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
reason = results
.map((result) =>
buildReasonWithVerboseMetricName(
- result[item],
+ group,
+ result[group],
buildFiredAlertReason,
nextState === AlertStates.WARNING
)
@@ -151,19 +152,23 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
*/
// } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) {
// reason = results
- // .map((result) => buildReasonWithVerboseMetricName(result[item], buildRecoveredAlertReason))
+ // .map((result) => buildReasonWithVerboseMetricName(group, result[group], buildRecoveredAlertReason))
// .join('\n');
}
if (alertOnNoData) {
if (nextState === AlertStates.NO_DATA) {
reason = results
- .filter((result) => result[item].isNoData)
- .map((result) => buildReasonWithVerboseMetricName(result[item], buildNoDataAlertReason))
+ .filter((result) => result[group].isNoData)
+ .map((result) =>
+ buildReasonWithVerboseMetricName(group, result[group], buildNoDataAlertReason)
+ )
.join('\n');
} else if (nextState === AlertStates.ERROR) {
reason = results
- .filter((result) => result[item].isError)
- .map((result) => buildReasonWithVerboseMetricName(result[item], buildErrorAlertReason))
+ .filter((result) => result[group].isError)
+ .map((result) =>
+ buildReasonWithVerboseMetricName(group, result[group], buildErrorAlertReason)
+ )
.join('\n');
}
}
@@ -175,7 +180,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
? WARNING_ACTIONS.id
: FIRED_ACTIONS.id;
- const alertInstance = alertInstanceFactory(`${item}`, reason);
+ const alertInstance = alertInstanceFactory(`${group}`, reason);
alertInstance.scheduleActions(
/**
* TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on
@@ -183,12 +188,12 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
*/
actionGroupId as unknown as InventoryMetricThresholdAllowedActionGroups,
{
- group: item,
+ group,
alertState: stateToAlertMessage[nextState],
reason,
timestamp: moment().toISOString(),
value: mapToConditionsLookup(results, (result) =>
- formatMetric(result[item].metric, result[item].currentValue)
+ formatMetric(result[group].metric, result[group].currentValue)
),
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
metric: mapToConditionsLookup(criteria, (c) => c.metric),
@@ -199,6 +204,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
});
const buildReasonWithVerboseMetricName = (
+ group: string,
resultItem: any,
buildReason: (r: any) => string,
useWarningThreshold?: boolean
@@ -206,6 +212,7 @@ const buildReasonWithVerboseMetricName = (
if (!resultItem) return '';
const resultWithVerboseMetricName = {
...resultItem,
+ group,
metric:
toMetricOpt(resultItem.metric)?.text ||
(resultItem.metric === 'custom'
diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts
index cd579b9965b66..f70e0a0140ce8 100644
--- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts
@@ -34,7 +34,7 @@ export const getReasonMessageForGroupedCountAlert = (
) =>
i18n.translate('xpack.infra.logs.alerting.threshold.groupedCountAlertReasonDescription', {
defaultMessage:
- '{groupName}: {actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries} } ({translatedComparator} {expectedCount}) match the conditions.',
+ '{actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries} } ({translatedComparator} {expectedCount}) match the conditions for {groupName}.',
values: {
actualCount,
expectedCount,
@@ -66,7 +66,7 @@ export const getReasonMessageForGroupedRatioAlert = (
) =>
i18n.translate('xpack.infra.logs.alerting.threshold.groupedRatioAlertReasonDescription', {
defaultMessage:
- '{groupName}: The log entries ratio is {actualRatio} ({translatedComparator} {expectedRatio}).',
+ 'The log entries ratio is {actualRatio} ({translatedComparator} {expectedRatio}) for {groupName}.',
values: {
actualRatio,
expectedRatio,
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
index af5f945eeb4bb..e4887e922bb66 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
@@ -143,9 +143,10 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
if (nextState === AlertStates.ALERT || nextState === AlertStates.WARNING) {
reason = alertResults
.map((result) =>
- buildFiredAlertReason(
- formatAlertResult(result[group], nextState === AlertStates.WARNING)
- )
+ buildFiredAlertReason({
+ ...formatAlertResult(result[group], nextState === AlertStates.WARNING),
+ group,
+ })
)
.join('\n');
/*
@@ -181,7 +182,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
if (nextState === AlertStates.NO_DATA) {
reason = alertResults
.filter((result) => result[group].isNoData)
- .map((result) => buildNoDataAlertReason(result[group]))
+ .map((result) => buildNoDataAlertReason({ ...result[group], group }))
.join('\n');
} else if (nextState === AlertStates.ERROR) {
reason = alertResults
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 95f6909b12f6c..f86167046c4c2 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -13580,15 +13580,12 @@
"xpack.infra.metrics.alerting.threshold.errorAlertReason": "{metric}のデータのクエリを試行しているときに、Elasticsearchが失敗しました",
"xpack.infra.metrics.alerting.threshold.errorState": "エラー",
"xpack.infra.metrics.alerting.threshold.fired": "アラート",
- "xpack.infra.metrics.alerting.threshold.firedAlertReason": "{metric}は{comparator} {threshold}のしきい値です(現在の値は{currentValue})",
"xpack.infra.metrics.alerting.threshold.gtComparator": "より大きい",
"xpack.infra.metrics.alerting.threshold.ltComparator": "より小さい",
- "xpack.infra.metrics.alerting.threshold.noDataAlertReason": "{metric}は過去{interval}にデータを報告していません",
"xpack.infra.metrics.alerting.threshold.noDataFormattedValue": "[データなし]",
"xpack.infra.metrics.alerting.threshold.noDataState": "データなし",
"xpack.infra.metrics.alerting.threshold.okState": "OK [回復済み]",
"xpack.infra.metrics.alerting.threshold.outsideRangeComparator": "の間にない",
- "xpack.infra.metrics.alerting.threshold.recoveredAlertReason": "{metric}は{comparator} {threshold}のしきい値です(現在の値は{currentValue})",
"xpack.infra.metrics.alerting.threshold.thresholdRange": "{a}と{b}",
"xpack.infra.metrics.alerting.threshold.warning": "警告",
"xpack.infra.metrics.alerting.threshold.warningState": "警告",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 80dad5a2c0c8b..6cfdc69c9e897 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -13769,15 +13769,12 @@
"xpack.infra.metrics.alerting.threshold.errorAlertReason": "Elasticsearch 尝试查询 {metric} 的数据时出现故障",
"xpack.infra.metrics.alerting.threshold.errorState": "错误",
"xpack.infra.metrics.alerting.threshold.fired": "告警",
- "xpack.infra.metrics.alerting.threshold.firedAlertReason": "{metric} {comparator}阈值 {threshold}(当前值为 {currentValue})",
"xpack.infra.metrics.alerting.threshold.gtComparator": "大于",
"xpack.infra.metrics.alerting.threshold.ltComparator": "小于",
- "xpack.infra.metrics.alerting.threshold.noDataAlertReason": "{metric} 在过去 {interval}中未报告数据",
"xpack.infra.metrics.alerting.threshold.noDataFormattedValue": "[无数据]",
"xpack.infra.metrics.alerting.threshold.noDataState": "无数据",
"xpack.infra.metrics.alerting.threshold.okState": "正常 [已恢复]",
"xpack.infra.metrics.alerting.threshold.outsideRangeComparator": "不介于",
- "xpack.infra.metrics.alerting.threshold.recoveredAlertReason": "{metric} 现在{comparator}阈值 {threshold}(当前值为 {currentValue})",
"xpack.infra.metrics.alerting.threshold.thresholdRange": "{a} 和 {b}",
"xpack.infra.metrics.alerting.threshold.warning": "警告",
"xpack.infra.metrics.alerting.threshold.warningState": "警告",
From fe17761ae8f644df5f10af1275fe0640eff7e4a2 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Tue, 19 Oct 2021 15:38:12 +0300
Subject: [PATCH 18/18] [VisEditors] Sets level for all registered deprecations
(#115505)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/kibana_legacy/server/index.ts | 5 ++++-
src/plugins/vis_types/metric/server/index.ts | 2 +-
src/plugins/vis_types/table/server/index.ts | 4 ++--
src/plugins/vis_types/tagcloud/server/index.ts | 2 +-
src/plugins/vis_types/timelion/server/index.ts | 15 ++++++++++-----
src/plugins/vis_types/timeseries/server/index.ts | 8 +++++---
src/plugins/vis_types/vega/server/index.ts | 6 ++++--
7 files changed, 27 insertions(+), 15 deletions(-)
diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts
index 15ae8547c73e1..3f731efbfe857 100644
--- a/src/plugins/kibana_legacy/server/index.ts
+++ b/src/plugins/kibana_legacy/server/index.ts
@@ -18,7 +18,10 @@ export const config: PluginConfigDescriptor = {
schema: configSchema,
deprecations: ({ renameFromRoot }) => [
// TODO: Remove deprecation once defaultAppId is deleted
- renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', { silent: true }),
+ renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', {
+ silent: true,
+ level: 'critical',
+ }),
(completeConfig, rootPath, addDeprecation) => {
if (
get(completeConfig, 'kibana.defaultAppId') === undefined &&
diff --git a/src/plugins/vis_types/metric/server/index.ts b/src/plugins/vis_types/metric/server/index.ts
index 740fe3426dd84..0b6768074d6cb 100644
--- a/src/plugins/vis_types/metric/server/index.ts
+++ b/src/plugins/vis_types/metric/server/index.ts
@@ -13,7 +13,7 @@ import { configSchema, ConfigSchema } from '../config';
export const config: PluginConfigDescriptor = {
schema: configSchema,
deprecations: ({ renameFromRoot }) => [
- renameFromRoot('metric_vis.enabled', 'vis_type_metric.enabled'),
+ renameFromRoot('metric_vis.enabled', 'vis_type_metric.enabled', { level: 'critical' }),
],
};
diff --git a/src/plugins/vis_types/table/server/index.ts b/src/plugins/vis_types/table/server/index.ts
index eed1134f3ff48..103586305139c 100644
--- a/src/plugins/vis_types/table/server/index.ts
+++ b/src/plugins/vis_types/table/server/index.ts
@@ -15,9 +15,9 @@ import { registerVisTypeTableUsageCollector } from './usage_collector';
export const config: PluginConfigDescriptor = {
schema: configSchema,
deprecations: ({ renameFromRoot, unused }) => [
- renameFromRoot('table_vis.enabled', 'vis_type_table.enabled'),
+ renameFromRoot('table_vis.enabled', 'vis_type_table.enabled', { level: 'critical' }),
// Unused property which should be removed after releasing Kibana v8.0:
- unused('legacyVisEnabled'),
+ unused('legacyVisEnabled', { level: 'critical' }),
],
};
diff --git a/src/plugins/vis_types/tagcloud/server/index.ts b/src/plugins/vis_types/tagcloud/server/index.ts
index 6899a333a8812..35c0a127c873f 100644
--- a/src/plugins/vis_types/tagcloud/server/index.ts
+++ b/src/plugins/vis_types/tagcloud/server/index.ts
@@ -13,7 +13,7 @@ import { configSchema, ConfigSchema } from '../config';
export const config: PluginConfigDescriptor = {
schema: configSchema,
deprecations: ({ renameFromRoot }) => [
- renameFromRoot('tagcloud.enabled', 'vis_type_tagcloud.enabled'),
+ renameFromRoot('tagcloud.enabled', 'vis_type_tagcloud.enabled', { level: 'critical' }),
],
};
diff --git a/src/plugins/vis_types/timelion/server/index.ts b/src/plugins/vis_types/timelion/server/index.ts
index ef7baf981de1a..b5985932188fb 100644
--- a/src/plugins/vis_types/timelion/server/index.ts
+++ b/src/plugins/vis_types/timelion/server/index.ts
@@ -13,12 +13,17 @@ import { TimelionPlugin } from './plugin';
export const config: PluginConfigDescriptor = {
schema: configSchema,
deprecations: ({ renameFromRoot, unused }) => [
- renameFromRoot('timelion_vis.enabled', 'vis_type_timelion.enabled'),
- renameFromRoot('timelion.enabled', 'vis_type_timelion.enabled'),
- renameFromRoot('timelion.graphiteUrls', 'vis_type_timelion.graphiteUrls'),
+ renameFromRoot('timelion_vis.enabled', 'vis_type_timelion.enabled', { level: 'critical' }),
+ renameFromRoot('timelion.enabled', 'vis_type_timelion.enabled', { level: 'critical' }),
+ renameFromRoot('timelion.graphiteUrls', 'vis_type_timelion.graphiteUrls', {
+ level: 'critical',
+ }),
// Unused properties which should be removed after releasing Kibana v8.0:
- renameFromRoot('timelion.ui.enabled', 'vis_type_timelion.ui.enabled', { silent: true }),
- unused('ui.enabled'),
+ renameFromRoot('timelion.ui.enabled', 'vis_type_timelion.ui.enabled', {
+ silent: true,
+ level: 'critical',
+ }),
+ unused('ui.enabled', { level: 'critical' }),
],
};
diff --git a/src/plugins/vis_types/timeseries/server/index.ts b/src/plugins/vis_types/timeseries/server/index.ts
index 0890b37e77926..ff2e54b31084b 100644
--- a/src/plugins/vis_types/timeseries/server/index.ts
+++ b/src/plugins/vis_types/timeseries/server/index.ts
@@ -15,17 +15,19 @@ export { VisTypeTimeseriesSetup } from './plugin';
export const config: PluginConfigDescriptor = {
deprecations: ({ unused, renameFromRoot }) => [
// In Kibana v7.8 plugin id was renamed from 'metrics' to 'vis_type_timeseries':
- renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled'),
+ renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled', { level: 'critical' }),
renameFromRoot('metrics.chartResolution', 'vis_type_timeseries.chartResolution', {
silent: true,
+ level: 'critical',
}),
renameFromRoot('metrics.minimumBucketSize', 'vis_type_timeseries.minimumBucketSize', {
silent: true,
+ level: 'critical',
}),
// Unused properties which should be removed after releasing Kibana v8.0:
- unused('chartResolution'),
- unused('minimumBucketSize'),
+ unused('chartResolution', { level: 'critical' }),
+ unused('minimumBucketSize', { level: 'critical' }),
],
schema: configSchema,
};
diff --git a/src/plugins/vis_types/vega/server/index.ts b/src/plugins/vis_types/vega/server/index.ts
index 156dec027372a..220d049c739ea 100644
--- a/src/plugins/vis_types/vega/server/index.ts
+++ b/src/plugins/vis_types/vega/server/index.ts
@@ -17,8 +17,10 @@ export const config: PluginConfigDescriptor = {
},
schema: configSchema,
deprecations: ({ renameFromRoot }) => [
- renameFromRoot('vega.enableExternalUrls', 'vis_type_vega.enableExternalUrls'),
- renameFromRoot('vega.enabled', 'vis_type_vega.enabled'),
+ renameFromRoot('vega.enableExternalUrls', 'vis_type_vega.enableExternalUrls', {
+ level: 'critical',
+ }),
+ renameFromRoot('vega.enabled', 'vis_type_vega.enabled', { level: 'critical' }),
],
};