From 355ec851341b490e71bf860bfcec8fa01a82530e Mon Sep 17 00:00:00 2001 From: Achyut Jhunjhunwala Date: Tue, 3 Oct 2023 09:55:22 +0200 Subject: [PATCH 01/24] [APM] Refactor transaction types API to use RollupIntervals and Metrics (#167500) ## Summary Resolves - https://github.com/elastic/kibana/issues/167020 This PR - - [x] Add logic to use the RollupIntervals and Document Source from TimeRange Metadata API to fetch Transaction Types instead of its own logic. - [x] Updates the Existing API test to use Synthtrace rather than Archives - [x] Renames the `getDocumentTypeFilterForTransactions` function to something more readable and fancy (Ground work for future PRs) ## How to review Better do it commit wise or only the commits from 28th September. The last commit does a function rename which has caused so many files to change. --- .../shared/search_bar/search_bar.test.tsx | 17 ++- .../apm_service/apm_service_context.tsx | 12 +++ .../use_service_transaction_types_fetcher.tsx | 12 ++- ...e_preferred_data_source_and_bucket_size.ts | 6 ++ .../helpers/create_es_client/document_type.ts | 4 +- .../server/lib/helpers/transactions/index.ts | 3 +- .../transaction_groups/get_coldstart_rate.ts | 4 +- .../get_transaction_duration_chart_preview.ts | 6 +- ...register_transaction_duration_rule_type.ts | 4 +- ...et_transaction_error_rate_chart_preview.ts | 4 +- ...gister_transaction_error_rate_rule_type.ts | 4 +- .../fetch_duration_histogram_range_steps.ts | 4 +- .../queries/fetch_duration_percentiles.ts | 4 +- .../queries/fetch_duration_ranges.ts | 4 +- .../get_transactions_per_minute.ts | 4 +- .../get_service_map_service_node_info.ts | 4 +- .../__snapshots__/queries.test.ts.snap | 7 +- .../get_derived_service_annotations.ts | 4 +- .../get_service_instance_metadata_details.ts | 6 +- ...ervice_instances_transaction_statistics.ts | 10 +- .../services/get_service_transaction_types.ts | 22 ++-- .../server/routes/services/queries.test.ts | 3 +- .../apm/server/routes/services/route.ts | 15 +-- .../get_summary_statistics.ts | 4 +- .../get_total_transactions_per_service.ts | 4 +- .../traces/get_top_traces_primary_stats.ts | 4 +- .../tests/feature_controls.spec.ts | 4 +- .../tests/services/transaction_types.spec.ts | 101 +++++++++++------- 28 files changed, 171 insertions(+), 109 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/search_bar/search_bar.test.tsx b/x-pack/plugins/apm/public/components/shared/search_bar/search_bar.test.tsx index fdfaabf9b5c0e2..5713390de48f7d 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar/search_bar.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar/search_bar.test.tsx @@ -20,6 +20,7 @@ import { renderWithTheme } from '../../../utils/test_helpers'; import { fromQuery } from '../links/url_helpers'; import { CoreStart } from '@kbn/core/public'; import { SearchBar } from './search_bar'; +import { ApmTimeRangeMetadataContextProvider } from '../../../context/time_range_metadata/time_range_metadata_context'; function setup({ urlParams, @@ -36,8 +37,12 @@ function setup({ }); const KibanaReactContext = createKibanaReactContext({ - usageCollection: { reportUiCounter: () => {} }, - dataViews: { get: async () => {} }, + usageCollection: { + reportUiCounter: () => {}, + }, + dataViews: { + get: async () => {}, + }, data: { query: { queryString: { @@ -75,9 +80,11 @@ function setup({ - - - + + + + + diff --git a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx index f7f4fe6d2e7848..aadf7092cccfea 100644 --- a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -8,6 +8,7 @@ import React, { createContext, ReactNode } from 'react'; import { useHistory } from 'react-router-dom'; import { History } from 'history'; +import { ApmDocumentType } from '../../../common/document_type'; import { getDefaultTransactionType } from '../../../common/transaction_types'; import { useServiceTransactionTypesFetcher } from './use_service_transaction_types_fetcher'; import { useServiceAgentFetcher } from './use_service_agent_fetcher'; @@ -17,6 +18,7 @@ import { useFallbackToTransactionsFetcher } from '../../hooks/use_fallback_to_tr import { replace } from '../../components/shared/links/url_helpers'; import { FETCH_STATUS } from '../../hooks/use_fetcher'; import { ServerlessType } from '../../../common/serverless'; +import { usePreferredDataSourceAndBucketSize } from '../../hooks/use_preferred_data_source_and_bucket_size'; export interface APMServiceContextValue { serviceName: string; @@ -67,11 +69,21 @@ export function ApmServiceContextProvider({ end, }); + const preferred = usePreferredDataSourceAndBucketSize({ + start, + end, + kuery, + type: ApmDocumentType.TransactionMetric, + numBuckets: 100, + }); + const { transactionTypes, status: transactionTypeStatus } = useServiceTransactionTypesFetcher({ serviceName, start, end, + documentType: preferred?.source.documentType, + rollupInterval: preferred?.source.rollupInterval, }); const currentTransactionType = getOrRedirectToTransactionType({ diff --git a/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx index fa00f7f45e743e..621a73ac3b037f 100644 --- a/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx @@ -6,6 +6,8 @@ */ import { useFetcher } from '../../hooks/use_fetcher'; +import { RollupInterval } from '../../../common/rollup'; +import { ApmTransactionDocumentType } from '../../../common/document_type'; const INITIAL_DATA = { transactionTypes: [] }; @@ -13,26 +15,30 @@ export function useServiceTransactionTypesFetcher({ serviceName, start, end, + documentType, + rollupInterval, }: { serviceName?: string; start?: string; end?: string; + documentType?: ApmTransactionDocumentType; + rollupInterval?: RollupInterval; }) { const { data = INITIAL_DATA, status } = useFetcher( (callApmApi) => { - if (serviceName && start && end) { + if (serviceName && start && end && documentType && rollupInterval) { return callApmApi( 'GET /internal/apm/services/{serviceName}/transaction_types', { params: { path: { serviceName }, - query: { start, end }, + query: { start, end, documentType, rollupInterval }, }, } ); } }, - [serviceName, start, end] + [serviceName, start, end, documentType, rollupInterval] ); return { transactionTypes: data.transactionTypes, status }; diff --git a/x-pack/plugins/apm/public/hooks/use_preferred_data_source_and_bucket_size.ts b/x-pack/plugins/apm/public/hooks/use_preferred_data_source_and_bucket_size.ts index 07ee3b88ad08c0..be99a537de011f 100644 --- a/x-pack/plugins/apm/public/hooks/use_preferred_data_source_and_bucket_size.ts +++ b/x-pack/plugins/apm/public/hooks/use_preferred_data_source_and_bucket_size.ts @@ -12,6 +12,12 @@ import { getBucketSize } from '../../common/utils/get_bucket_size'; import { getPreferredBucketSizeAndDataSource } from '../../common/utils/get_preferred_bucket_size_and_data_source'; import { useTimeRangeMetadata } from '../context/time_range_metadata/use_time_range_metadata_context'; +/** + * Hook to get the source and interval based on Time Range Metadata API + * + * @param {number} numBuckets - The number of buckets. Should be 20 for SparkPlots or 100 for Other charts. + + */ export function usePreferredDataSourceAndBucketSize< TDocumentType extends | ApmDocumentType.ServiceTransactionMetric diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/document_type.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/document_type.ts index 97d3578b639006..a3f5ff8d5683c0 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/document_type.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/document_type.ts @@ -15,7 +15,7 @@ import { import { RollupInterval } from '../../../../common/rollup'; import { termQuery } from '../../../../common/utils/term_query'; import { getDocumentTypeFilterForServiceDestinationStatistics } from '../spans/get_is_using_service_destination_metrics'; -import { getDocumentTypeFilterForTransactions } from '../transactions'; +import { getBackwardCompatibleDocumentTypeFilter } from '../transactions'; const defaultRollupIntervals = [ RollupInterval.OneMinute, @@ -66,7 +66,7 @@ const documentTypeConfigMap: Record< bool: { filter: rollupInterval === RollupInterval.OneMinute - ? getDocumentTypeFilterForTransactions(true) + ? getBackwardCompatibleDocumentTypeFilter(true) : getDefaultFilter('transaction', rollupInterval), }, }), diff --git a/x-pack/plugins/apm/server/lib/helpers/transactions/index.ts b/x-pack/plugins/apm/server/lib/helpers/transactions/index.ts index 791ab5ad2f666e..182d2419c25434 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transactions/index.ts @@ -121,7 +121,8 @@ export function getDurationFieldForTransactions( return TRANSACTION_DURATION; } -export function getDocumentTypeFilterForTransactions( +// The function returns Document type filter for 1m Transaction Metrics +export function getBackwardCompatibleDocumentTypeFilter( searchAggregatedTransactions: boolean ) { return searchAggregatedTransactions diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_coldstart_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_coldstart_rate.ts index e4e0cc16856150..f205beae5c4e73 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_coldstart_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_coldstart_rate.ts @@ -17,7 +17,7 @@ import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_pr import { environmentQuery } from '../../../common/utils/environment_query'; import { Coordinate } from '../../../typings/timeseries'; import { - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, getProcessorEventForTransactions, } from '../helpers/transactions'; import { getBucketSizeForAggregatedTransactions } from '../helpers/get_bucket_size_for_aggregated_transactions'; @@ -66,7 +66,7 @@ export async function getColdstartRate({ { exists: { field: FAAS_COLDSTART } }, ...(transactionName ? termQuery(TRANSACTION_NAME, transactionName) : []), ...termQuery(TRANSACTION_TYPE, transactionType), - ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), + ...getBackwardCompatibleDocumentTypeFilter(searchAggregatedTransactions), ...rangeQuery(startWithOffset, endWithOffset), ...environmentQuery(environment), ...kqlQuery(kuery), diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/get_transaction_duration_chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/get_transaction_duration_chart_preview.ts index bf191c5213cf0f..db968bdfcc2cef 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/get_transaction_duration_chart_preview.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/get_transaction_duration_chart_preview.ts @@ -24,7 +24,7 @@ import { environmentQuery } from '../../../../../common/utils/environment_query' import { AlertParams, PreviewChartResponse } from '../../route'; import { getSearchTransactionsEvents, - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../../../lib/helpers/transactions'; @@ -89,7 +89,9 @@ export async function getTransactionDurationChartPreview({ ...termFilterQuery, ...getParsedFilterQuery(searchConfiguration?.query?.query as string), ...rangeQuery(start, end), - ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), + ...getBackwardCompatibleDocumentTypeFilter( + searchAggregatedTransactions + ), ] as QueryDslQueryContainer[], }, }; diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts index 2991a58f577bed..cbfb5db6271008 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts @@ -50,7 +50,7 @@ import { getDurationFormatter, } from '../../../../../common/utils/formatters'; import { - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, getDurationFieldForTransactions, } from '../../../../lib/helpers/transactions'; import { apmActionVariables } from '../../action_variables'; @@ -167,7 +167,7 @@ export function registerTransactionDurationRuleType({ }, }, }, - ...getDocumentTypeFilterForTransactions( + ...getBackwardCompatibleDocumentTypeFilter( searchAggregatedTransactions ), ...termFilterQuery, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/get_transaction_error_rate_chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/get_transaction_error_rate_chart_preview.ts index 3ffa001fccc9db..853252e6ef453f 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/get_transaction_error_rate_chart_preview.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/get_transaction_error_rate_chart_preview.ts @@ -21,7 +21,7 @@ import { environmentQuery } from '../../../../../common/utils/environment_query' import { AlertParams, PreviewChartResponse } from '../../route'; import { getSearchTransactionsEvents, - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, getProcessorEventForTransactions, } from '../../../../lib/helpers/transactions'; import { APMConfig } from '../../../..'; @@ -96,7 +96,7 @@ export async function getTransactionErrorRateChartPreview({ searchConfiguration?.query?.query as string ), ...rangeQuery(start, end), - ...getDocumentTypeFilterForTransactions( + ...getBackwardCompatibleDocumentTypeFilter( searchAggregatedTransactions ), { diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts index 08385ffdec3a94..d7c700c42071c5 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts @@ -50,7 +50,7 @@ import { asDecimalOrInteger, getAlertUrlTransaction, } from '../../../../../common/utils/formatters'; -import { getDocumentTypeFilterForTransactions } from '../../../../lib/helpers/transactions'; +import { getBackwardCompatibleDocumentTypeFilter } from '../../../../lib/helpers/transactions'; import { apmActionVariables } from '../../action_variables'; import { alertingEsClient } from '../../alerting_es_client'; import { @@ -169,7 +169,7 @@ export function registerTransactionErrorRateRuleType({ }, }, }, - ...getDocumentTypeFilterForTransactions( + ...getBackwardCompatibleDocumentTypeFilter( searchAggregatedTransactions ), { diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_histogram_range_steps.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_histogram_range_steps.ts index 7a08697d7e76ac..24c775bb32f14a 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_histogram_range_steps.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_histogram_range_steps.ts @@ -13,7 +13,7 @@ import { LatencyDistributionChartType } from '../../../../common/latency_distrib import { getCommonCorrelationsQuery } from './get_common_correlations_query'; import { getDurationField, getEventType } from '../utils'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; -import { getDocumentTypeFilterForTransactions } from '../../../lib/helpers/transactions'; +import { getBackwardCompatibleDocumentTypeFilter } from '../../../lib/helpers/transactions'; 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. @@ -66,7 +66,7 @@ export const fetchDurationHistogramRangeSteps = async ({ const filteredQuery = searchMetrics ? { bool: { - filter: [query, ...getDocumentTypeFilterForTransactions(true)], + filter: [query, ...getBackwardCompatibleDocumentTypeFilter(true)], }, } : query; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.ts index 588111901a7eb2..19ad2d8a1758de 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.ts @@ -11,7 +11,7 @@ import { getCommonCorrelationsQuery } from './get_common_correlations_query'; import { CommonCorrelationsQueryParams } from '../../../../common/correlations/types'; import { getDurationField, getEventType } from '../utils'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; -import { getDocumentTypeFilterForTransactions } from '../../../lib/helpers/transactions'; +import { getBackwardCompatibleDocumentTypeFilter } from '../../../lib/helpers/transactions'; export const fetchDurationPercentiles = async ({ chartType, @@ -36,7 +36,7 @@ export const fetchDurationPercentiles = async ({ const filteredQuery = searchMetrics ? { bool: { - filter: [query, ...getDocumentTypeFilterForTransactions(true)], + filter: [query, ...getBackwardCompatibleDocumentTypeFilter(true)], }, } : query; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_ranges.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_ranges.ts index af7caa207d92a6..84c1669f2cee90 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_ranges.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_ranges.ts @@ -12,7 +12,7 @@ import { getCommonCorrelationsQuery } from './get_common_correlations_query'; import { Environment } from '../../../../common/environment_rt'; import { getDurationField, getEventType } from '../utils'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; -import { getDocumentTypeFilterForTransactions } from '../../../lib/helpers/transactions'; +import { getBackwardCompatibleDocumentTypeFilter } from '../../../lib/helpers/transactions'; export const fetchDurationRanges = async ({ rangeSteps, @@ -42,7 +42,7 @@ export const fetchDurationRanges = async ({ const filteredQuery = searchMetrics ? { bool: { - filter: [query, ...getDocumentTypeFilterForTransactions(true)], + filter: [query, ...getBackwardCompatibleDocumentTypeFilter(true)], }, } : query; diff --git a/x-pack/plugins/apm/server/routes/observability_overview/get_transactions_per_minute.ts b/x-pack/plugins/apm/server/routes/observability_overview/get_transactions_per_minute.ts index 23c5b18297c9dc..ca9555a721ab65 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview/get_transactions_per_minute.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview/get_transactions_per_minute.ts @@ -9,7 +9,7 @@ import { rangeQuery } from '@kbn/observability-plugin/server'; import { isDefaultTransactionType } from '../../../common/transaction_types'; import { TRANSACTION_TYPE } from '../../../common/es_fields/apm'; import { - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, getProcessorEventForTransactions, } from '../../lib/helpers/transactions'; import { calculateThroughputWithRange } from '../../lib/helpers/calculate_throughput'; @@ -45,7 +45,7 @@ export async function getTransactionsPerMinute({ bool: { filter: [ ...rangeQuery(start, end), - ...getDocumentTypeFilterForTransactions( + ...getBackwardCompatibleDocumentTypeFilter( searchAggregatedTransactions ), ], diff --git a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts index f9db8bdeb09fb6..434d1755afce8c 100644 --- a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts @@ -19,7 +19,7 @@ import { environmentQuery } from '../../../common/utils/environment_query'; import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; import { getBucketSizeForAggregatedTransactions } from '../../lib/helpers/get_bucket_size_for_aggregated_transactions'; import { - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../lib/helpers/transactions'; @@ -184,7 +184,7 @@ async function getTransactionStats({ bool: { filter: [ ...filter, - ...getDocumentTypeFilterForTransactions( + ...getBackwardCompatibleDocumentTypeFilter( searchAggregatedTransactions ), { diff --git a/x-pack/plugins/apm/server/routes/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/routes/services/__snapshots__/queries.test.ts.snap index 80dd33b965ac1d..03c3f038ad311a 100644 --- a/x-pack/plugins/apm/server/routes/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/routes/services/__snapshots__/queries.test.ts.snap @@ -270,8 +270,11 @@ Array [ exports[`services queries fetches the service transaction types 1`] = ` Object { "apm": Object { - "events": Array [ - "transaction", + "sources": Array [ + Object { + "documentType": "transactionMetric", + "rollupInterval": "1m", + }, ], }, "body": Object { diff --git a/x-pack/plugins/apm/server/routes/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/routes/services/annotations/get_derived_service_annotations.ts index ce5a5eb9592176..ae2ac42ca58da9 100644 --- a/x-pack/plugins/apm/server/routes/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/routes/services/annotations/get_derived_service_annotations.ts @@ -15,7 +15,7 @@ import { } from '../../../../common/es_fields/apm'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, getProcessorEventForTransactions, } from '../../../lib/helpers/transactions'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; @@ -37,7 +37,7 @@ export async function getDerivedServiceAnnotations({ }) { const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, - ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), + ...getBackwardCompatibleDocumentTypeFilter(searchAggregatedTransactions), ...environmentQuery(environment), ]; diff --git a/x-pack/plugins/apm/server/routes/services/get_service_instance_metadata_details.ts b/x-pack/plugins/apm/server/routes/services/get_service_instance_metadata_details.ts index 237677ae64efcd..e24898f57dfac4 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_instance_metadata_details.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_instance_metadata_details.ts @@ -14,7 +14,7 @@ import { } from '../../../common/es_fields/apm'; import { maybe } from '../../../common/utils/maybe'; import { - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, getProcessorEventForTransactions, } from '../../lib/helpers/transactions'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; @@ -109,7 +109,9 @@ export async function getServiceInstanceMetadataDetails({ size: 1, query: { bool: { - filter: filter.concat(getDocumentTypeFilterForTransactions(true)), + filter: filter.concat( + getBackwardCompatibleDocumentTypeFilter(true) + ), }, }, }, diff --git a/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts b/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts index 2b46b68eaf96a2..ab687382e767bf 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts @@ -17,7 +17,7 @@ import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { Coordinate } from '../../../../typings/timeseries'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../../lib/helpers/transactions'; @@ -114,11 +114,15 @@ export async function getServiceInstancesTransactionStatistics< filter: [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), + ...getBackwardCompatibleDocumentTypeFilter( + searchAggregatedTransactions + ), ...rangeQuery(startWithOffset, endWithOffset), ...environmentQuery(environment), ...kqlQuery(kuery), - ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), + ...getBackwardCompatibleDocumentTypeFilter( + searchAggregatedTransactions + ), ...(isComparisonSearch && serviceNodeIds ? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }] : []), diff --git a/x-pack/plugins/apm/server/routes/services/get_service_transaction_types.ts b/x-pack/plugins/apm/server/routes/services/get_service_transaction_types.ts index a4e305fc44ad34..87f13151e34baa 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_transaction_types.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_transaction_types.ts @@ -6,12 +6,10 @@ */ import { rangeQuery } from '@kbn/observability-plugin/server'; +import { ApmServiceTransactionDocumentType } from '../../../common/document_type'; import { SERVICE_NAME, TRANSACTION_TYPE } from '../../../common/es_fields/apm'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; -import { - getDocumentTypeFilterForTransactions, - getProcessorEventForTransactions, -} from '../../lib/helpers/transactions'; +import { RollupInterval } from '../../../common/rollup'; export interface ServiceTransactionTypesResponse { transactionTypes: string[]; @@ -20,19 +18,26 @@ export interface ServiceTransactionTypesResponse { export async function getServiceTransactionTypes({ apmEventClient, serviceName, - searchAggregatedTransactions, start, end, + documentType, + rollupInterval, }: { serviceName: string; apmEventClient: APMEventClient; - searchAggregatedTransactions: boolean; start: number; end: number; + documentType: ApmServiceTransactionDocumentType; + rollupInterval: RollupInterval; }): Promise { const params = { apm: { - events: [getProcessorEventForTransactions(searchAggregatedTransactions)], + sources: [ + { + documentType, + rollupInterval, + }, + ], }, body: { track_total_hits: false, @@ -40,9 +45,6 @@ export async function getServiceTransactionTypes({ query: { bool: { filter: [ - ...getDocumentTypeFilterForTransactions( - searchAggregatedTransactions - ), { term: { [SERVICE_NAME]: serviceName } }, ...rangeQuery(start, end), ], diff --git a/x-pack/plugins/apm/server/routes/services/queries.test.ts b/x-pack/plugins/apm/server/routes/services/queries.test.ts index b3da9b9264f99a..7f04d59639b31e 100644 --- a/x-pack/plugins/apm/server/routes/services/queries.test.ts +++ b/x-pack/plugins/apm/server/routes/services/queries.test.ts @@ -42,9 +42,10 @@ describe('services queries', () => { getServiceTransactionTypes({ serviceName: 'foo', apmEventClient: mockApmEventClient, - searchAggregatedTransactions: false, start: 0, end: 50000, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, }) ); diff --git a/x-pack/plugins/apm/server/routes/services/route.ts b/x-pack/plugins/apm/server/routes/services/route.ts index 52c04271bf31a6..ac394dbe467f50 100644 --- a/x-pack/plugins/apm/server/routes/services/route.ts +++ b/x-pack/plugins/apm/server/routes/services/route.ts @@ -328,27 +328,22 @@ const serviceTransactionTypesRoute = createApmServerRoute({ path: t.type({ serviceName: t.string, }), - query: rangeRt, + query: t.intersection([rangeRt, serviceTransactionDataSourceRt]), }), options: { tags: ['access:apm'] }, handler: async (resources): Promise => { const apmEventClient = await getApmEventClient(resources); - const { params, config } = resources; + const { params } = resources; const { serviceName } = params.path; - const { start, end } = params.query; + const { start, end, documentType, rollupInterval } = params.query; return getServiceTransactionTypes({ serviceName, apmEventClient, - searchAggregatedTransactions: await getSearchTransactionsEvents({ - apmEventClient, - config, - start, - end, - kuery: '', - }), start, end, + documentType, + rollupInterval, }); }, }); diff --git a/x-pack/plugins/apm/server/routes/storage_explorer/get_summary_statistics.ts b/x-pack/plugins/apm/server/routes/storage_explorer/get_summary_statistics.ts index 56ec4baf447bb6..565eb2a107ea1f 100644 --- a/x-pack/plugins/apm/server/routes/storage_explorer/get_summary_statistics.ts +++ b/x-pack/plugins/apm/server/routes/storage_explorer/get_summary_statistics.ts @@ -25,7 +25,7 @@ import { RandomSampler } from '../../lib/helpers/get_random_sampler'; import { SERVICE_NAME, TIER, INDEX } from '../../../common/es_fields/apm'; import { environmentQuery } from '../../../common/utils/environment_query'; import { - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, getProcessorEventForTransactions, getDurationFieldForTransactions, isRootTransaction, @@ -65,7 +65,7 @@ async function getTracesPerMinute({ query: { bool: { filter: [ - ...getDocumentTypeFilterForTransactions( + ...getBackwardCompatibleDocumentTypeFilter( searchAggregatedTransactions ), ...environmentQuery(environment), diff --git a/x-pack/plugins/apm/server/routes/storage_explorer/get_total_transactions_per_service.ts b/x-pack/plugins/apm/server/routes/storage_explorer/get_total_transactions_per_service.ts index b1aa823da569cc..7b232edfc54151 100644 --- a/x-pack/plugins/apm/server/routes/storage_explorer/get_total_transactions_per_service.ts +++ b/x-pack/plugins/apm/server/routes/storage_explorer/get_total_transactions_per_service.ts @@ -11,7 +11,7 @@ import { } from '@kbn/observability-plugin/server'; import { getProcessorEventForTransactions, - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, } from '../../lib/helpers/transactions'; import { SERVICE_NAME, TIER } from '../../../common/es_fields/apm'; import { @@ -55,7 +55,7 @@ export async function getTotalTransactionsPerService({ query: { bool: { filter: [ - ...getDocumentTypeFilterForTransactions( + ...getBackwardCompatibleDocumentTypeFilter( searchAggregatedTransactions ), ...environmentQuery(environment), diff --git a/x-pack/plugins/apm/server/routes/traces/get_top_traces_primary_stats.ts b/x-pack/plugins/apm/server/routes/traces/get_top_traces_primary_stats.ts index 6a4b9ce0edbfab..6caa7b47488001 100644 --- a/x-pack/plugins/apm/server/routes/traces/get_top_traces_primary_stats.ts +++ b/x-pack/plugins/apm/server/routes/traces/get_top_traces_primary_stats.ts @@ -19,7 +19,7 @@ import { calculateImpactBuilder } from './calculate_impact_builder'; import { calculateThroughputWithRange } from '../../lib/helpers/calculate_throughput'; import { getDurationFieldForTransactions, - getDocumentTypeFilterForTransactions, + getBackwardCompatibleDocumentTypeFilter, getProcessorEventForTransactions, isRootTransaction, } from '../../lib/helpers/transactions'; @@ -87,7 +87,7 @@ export async function getTopTracesPrimaryStats({ bool: { filter: [ ...termQuery(TRANSACTION_NAME, transactionName), - ...getDocumentTypeFilterForTransactions( + ...getBackwardCompatibleDocumentTypeFilter( searchAggregatedTransactions ), ...rangeQuery(start, end), diff --git a/x-pack/test/apm_api_integration/tests/feature_controls.spec.ts b/x-pack/test/apm_api_integration/tests/feature_controls.spec.ts index fb908f7bbe9c89..5ffc8c29c34956 100644 --- a/x-pack/test/apm_api_integration/tests/feature_controls.spec.ts +++ b/x-pack/test/apm_api_integration/tests/feature_controls.spec.ts @@ -117,7 +117,9 @@ export default function featureControlsTests({ getService }: FtrProviderContext) expectResponse: expect200, }, { - req: { url: `/internal/apm/services/foo/transaction_types?start=${start}&end=${end}` }, + req: { + url: `/internal/apm/services/foo/transaction_types?start=${start}&end=${end}&documentType=transactionMetric&rollupInterval=1m`, + }, expectForbidden: expect403, expectResponse: expect200, }, diff --git a/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts b/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts index bd1329cbe3e1ae..e6ae78d64e7f1d 100644 --- a/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts @@ -6,31 +6,44 @@ */ import expect from '@kbn/expect'; -import archives from '../../common/fixtures/es_archiver/archives_metadata'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); + const synthtrace = getService('synthtraceEsClient'); - const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; + const start = '2023-10-28T00:00:00.000Z'; + const end = '2023-10-28T00:14:59.999Z'; + + const serviceName = 'opbeans-node'; + + async function getTransactionTypes() { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transaction_types', + params: { + path: { serviceName }, + query: { + start, + end, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + }, + }, + }); + + return response; + } registry.when( 'Transaction types when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/transaction_types', - params: { - path: { serviceName: 'opbeans-node' }, - query: { - start, - end, - }, - }, - }); + const response = await getTransactionTypes(); expect(response.status).to.be(200); @@ -39,34 +52,40 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when( - 'Transaction types when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - it('handles empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/transaction_types', - params: { - path: { serviceName: 'opbeans-node' }, - query: { - start, - end, - }, - }, - }); + registry.when('Transaction types when data is loaded', { config: 'basic', archives: [] }, () => { + before(async () => { + const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval( + '1m' + ); - expect(response.status).to.be(200); - expect(response.body.transactionTypes.length).to.be.greaterThan(0); + const instance = apm.service(serviceName, 'production', 'node').instance('instance'); - expectSnapshot(response.body).toMatchInline(` - Object { - "transactionTypes": Array [ - "request", - "Worker", - ], - } - `); - }); - } - ); + await synthtrace.index([ + interval.rate(3).generator((timestamp) => { + return instance + .transaction({ transactionName: 'GET /api', transactionType: 'request' }) + .duration(1000) + .outcome('success') + .timestamp(timestamp); + }), + interval.rate(1).generator((timestamp) => { + return instance + .transaction({ transactionName: 'rm -rf *', transactionType: 'worker' }) + .duration(100) + .outcome('failure') + .timestamp(timestamp); + }), + ]); + }); + + after(() => synthtrace.clean()); + it('displays available tx types', async () => { + const response = await getTransactionTypes(); + + expect(response.status).to.be(200); + expect(response.body.transactionTypes.length).to.be.greaterThan(0); + + expect(response.body.transactionTypes).to.eql(['request', 'worker']); + }); + }); } From 87287fcd05699e9491a93b5674def50432648120 Mon Sep 17 00:00:00 2001 From: Jordan <51442161+JordanSh@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:11:06 +0300 Subject: [PATCH 02/24] [Cloud Security] Add copy and paste fields to the azure guide (#167775) --- .../post_install_azure_arm_template_modal.tsx | 23 +++++++---- .../azure_arm_template_instructions.tsx | 6 +++ .../steps/compute_steps.tsx | 1 + ..._azure_arm_template_managed_agent_step.tsx | 5 +++ .../components/azure_arm_template_guide.tsx | 41 ++++++++++++++++++- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_azure_arm_template_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_azure_arm_template_modal.tsx index 747a894a647fe0..52022f95ed5758 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_azure_arm_template_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_azure_arm_template_modal.tsx @@ -35,18 +35,21 @@ export const PostInstallAzureArmTemplateModal: React.FunctionComponent<{ agentPolicy: AgentPolicy; packagePolicy: PackagePolicy; }> = ({ onConfirm, onCancel, agentPolicy, packagePolicy }) => { - const { data: apyKeysData } = useQuery(['cloudFormationApiKeys'], () => - sendGetEnrollmentAPIKeys({ - page: 1, - perPage: 1, - kuery: `policy_id:${agentPolicy.id}`, - }) + const { data: apyKeysData } = useQuery( + ['azureArmTemplateApiKeys', { agentPolicyId: agentPolicy.id }], + () => + sendGetEnrollmentAPIKeys({ + page: 1, + perPage: 1, + kuery: `policy_id:${agentPolicy.id}`, + }) ); const azureArmTemplateProps = getAzureArmPropsFromPackagePolicy(packagePolicy); + const enrollmentToken = apyKeysData?.data?.items[0]?.api_key; const { azureArmTemplateUrl, error, isError, isLoading } = useCreateAzureArmTemplateUrl({ - enrollmentAPIKey: apyKeysData?.data?.items[0]?.api_key, + enrollmentAPIKey: enrollmentToken, azureArmTemplateProps, }); @@ -62,7 +65,11 @@ export const PostInstallAzureArmTemplateModal: React.FunctionComponent<{ - + {error && isError && ( <> diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/azure_arm_template_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/azure_arm_template_instructions.tsx index e213e990d10f20..a4cb5ee222f196 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/azure_arm_template_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/azure_arm_template_instructions.tsx @@ -10,6 +10,8 @@ import { EuiButton, EuiSpacer, EuiCallOut, EuiSkeletonText } from '@elastic/eui' import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import type { AgentPolicy } from '../../../common'; + import { AzureArmTemplateGuide } from '../azure_arm_template_guide'; import { useCreateAzureArmTemplateUrl } from '../../hooks/use_create_azure_arm_template_url'; @@ -19,10 +21,12 @@ import type { CloudSecurityIntegration } from './types'; interface Props { enrollmentAPIKey?: string; cloudSecurityIntegration: CloudSecurityIntegration; + agentPolicy?: AgentPolicy; } export const AzureArmTemplateInstructions: React.FunctionComponent = ({ enrollmentAPIKey, cloudSecurityIntegration, + agentPolicy, }) => { const { isLoading, azureArmTemplateUrl, error, isError } = useCreateAzureArmTemplateUrl({ enrollmentAPIKey, @@ -52,6 +56,8 @@ export const AzureArmTemplateInstructions: React.FunctionComponent = ({ > = ({ apiKeyData, enrollToken, cloudSecurityIntegration, + agentPolicy, }) ); } else { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_azure_arm_template_managed_agent_step.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_azure_arm_template_managed_agent_step.tsx index a24e958b389161..4ef1ecdf13c8fa 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_azure_arm_template_managed_agent_step.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_azure_arm_template_managed_agent_step.tsx @@ -11,6 +11,8 @@ import { i18n } from '@kbn/i18n'; import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; +import type { AgentPolicy } from '../../../../common'; + import { AzureArmTemplateInstructions } from '../azure_arm_template_instructions'; import type { GetOneEnrollmentAPIKeyResponse } from '../../../../common/types/rest_spec/enrollment_api_key'; @@ -23,12 +25,14 @@ export const InstallAzureArmTemplateManagedAgentStep = ({ enrollToken, isComplete, cloudSecurityIntegration, + agentPolicy, }: { selectedApiKeyId?: string; apiKeyData?: GetOneEnrollmentAPIKeyResponse | null; enrollToken?: string; isComplete?: boolean; cloudSecurityIntegration?: CloudSecurityIntegration | undefined; + agentPolicy?: AgentPolicy; }): EuiContainedStepProps => { const nonCompleteStatus = selectedApiKeyId ? undefined : 'disabled'; const status = isComplete ? 'complete' : nonCompleteStatus; @@ -44,6 +48,7 @@ export const InstallAzureArmTemplateManagedAgentStep = ({ ) : ( diff --git a/x-pack/plugins/fleet/public/components/azure_arm_template_guide.tsx b/x-pack/plugins/fleet/public/components/azure_arm_template_guide.tsx index 7cfb6542a02fef..cc8a741f997621 100644 --- a/x-pack/plugins/fleet/public/components/azure_arm_template_guide.tsx +++ b/x-pack/plugins/fleet/public/components/azure_arm_template_guide.tsx @@ -6,9 +6,13 @@ */ import React from 'react'; -import { EuiLink, EuiText } from '@elastic/eui'; +import { EuiCodeBlock, EuiDescriptionList, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import type { AgentPolicy } from '../../common'; +import { useFleetServerHostsForPolicy } from '../hooks'; + +import { useAgentPolicyWithPackagePolicies } from './agent_enrollment_flyout/hooks'; import type { CloudSecurityIntegrationAzureAccountType } from './agent_enrollment_flyout/types'; const azureResourceManagerLink = @@ -16,9 +20,17 @@ const azureResourceManagerLink = export const AzureArmTemplateGuide = ({ azureAccountType, + agentPolicy, + enrollmentToken = '', }: { azureAccountType?: CloudSecurityIntegrationAzureAccountType; + agentPolicy?: AgentPolicy; + enrollmentToken?: string; }) => { + const { agentPolicyWithPackagePolicies } = useAgentPolicyWithPackagePolicies(agentPolicy?.id); + const { fleetServerHosts } = useFleetServerHostsForPolicy(agentPolicyWithPackagePolicies); + const fleetServerHost = fleetServerHosts[0]; + return (

@@ -65,6 +77,33 @@ export const AzureArmTemplateGuide = ({ defaultMessage="Click the Launch ARM Template button below." /> +

  • + + + + {fleetServerHost} + + ), + }, + { + title: 'Enrollment Token', + description: ( + + {enrollmentToken} + + ), + }, + ]} + /> +
  • From eddcdbea2458f48d8831cd9473064a9a23984d1b Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 3 Oct 2023 09:11:59 +0100 Subject: [PATCH 03/24] [ML] Catching registration errors after license change (#167622) We register our cases integration after checking that the license level is platinum or trial, however if the license changes from platinum -> basic and then back again, we attempt to register them again. This throws an error during kibana start up. To test, change the license level platinum/trial -> basic and then basic -> platinum. There will now be a warning the log `ML failed to register cases persistable state for ml_anomaly_swimlane` Also updates the AIOPs change point cases registration which also should only be registered when running with a platinum or trial license. --- x-pack/plugins/aiops/server/plugin.ts | 13 ++--- x-pack/plugins/aiops/server/register_cases.ts | 24 +++++++++ x-pack/plugins/aiops/server/types.ts | 4 +- .../plugins/ml/server/lib/register_cases.ts | 32 +++++++++--- .../lib/register_sample_data_set_links.ts | 52 +++++++++++-------- x-pack/plugins/ml/server/plugin.ts | 4 +- .../registered_persistable_state_basic.ts | 1 - 7 files changed, 88 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/aiops/server/register_cases.ts diff --git a/x-pack/plugins/aiops/server/plugin.ts b/x-pack/plugins/aiops/server/plugin.ts index 2adcb2aaaa88b8..9eb613d7e3524d 100755 --- a/x-pack/plugins/aiops/server/plugin.ts +++ b/x-pack/plugins/aiops/server/plugin.ts @@ -10,8 +10,6 @@ import { Subscription } from 'rxjs'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; import type { DataRequestHandlerContext } from '@kbn/data-plugin/server'; import type { UsageCounter } from '@kbn/usage-collection-plugin/server'; - -import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '../common/constants'; import { PLUGIN_ID } from '../common'; import { isActiveLicense } from './lib/license'; import { @@ -24,6 +22,7 @@ import { import { defineLogRateAnalysisRoute } from './routes'; import { defineLogCategorizationRoutes } from './routes/log_categorization'; +import { registerCasesPersistableState } from './register_cases'; export class AiopsPlugin implements Plugin @@ -49,6 +48,10 @@ export class AiopsPlugin const aiopsLicense: AiopsLicense = { isActivePlatinumLicense: false }; this.licenseSubscription = plugins.licensing.license$.subscribe(async (license) => { aiopsLicense.isActivePlatinumLicense = isActiveLicense('platinum', license); + + if (aiopsLicense.isActivePlatinumLicense) { + registerCasesPersistableState(plugins.cases, this.logger); + } }); const router = core.http.createRouter(); @@ -59,12 +62,6 @@ export class AiopsPlugin defineLogCategorizationRoutes(router, aiopsLicense, this.usageCounter); }); - if (plugins.cases) { - plugins.cases.attachmentFramework.registerPersistableState({ - id: CASES_ATTACHMENT_CHANGE_POINT_CHART, - }); - } - return {}; } diff --git a/x-pack/plugins/aiops/server/register_cases.ts b/x-pack/plugins/aiops/server/register_cases.ts new file mode 100644 index 00000000000000..db05c0200d426a --- /dev/null +++ b/x-pack/plugins/aiops/server/register_cases.ts @@ -0,0 +1,24 @@ +/* + * 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 { Logger } from '@kbn/core/server'; +import type { CasesSetup } from '@kbn/cases-plugin/server'; +import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '../common/constants'; + +export function registerCasesPersistableState(cases: CasesSetup | undefined, logger: Logger) { + if (cases) { + try { + cases.attachmentFramework.registerPersistableState({ + id: CASES_ATTACHMENT_CHANGE_POINT_CHART, + }); + } catch (error) { + logger.warn( + `AIOPs failed to register cases persistable state for ${CASES_ATTACHMENT_CHANGE_POINT_CHART}` + ); + } + } +} diff --git a/x-pack/plugins/aiops/server/types.ts b/x-pack/plugins/aiops/server/types.ts index 562f6f7535b87e..2315b0ab01d699 100755 --- a/x-pack/plugins/aiops/server/types.ts +++ b/x-pack/plugins/aiops/server/types.ts @@ -6,13 +6,13 @@ */ import type { PluginSetup, PluginStart } from '@kbn/data-plugin/server'; -import type { LicensingPluginStart } from '@kbn/licensing-plugin/server'; +import type { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; import type { CasesSetup } from '@kbn/cases-plugin/server'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; export interface AiopsPluginSetupDeps { data: PluginSetup; - licensing: LicensingPluginStart; + licensing: LicensingPluginSetup; cases?: CasesSetup; usageCollection?: UsageCollectionSetup; } diff --git a/x-pack/plugins/ml/server/lib/register_cases.ts b/x-pack/plugins/ml/server/lib/register_cases.ts index b8a226afd9bc86..fabd317d1d1bf7 100644 --- a/x-pack/plugins/ml/server/lib/register_cases.ts +++ b/x-pack/plugins/ml/server/lib/register_cases.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { Logger } from '@kbn/core/server'; import type { CasesSetup } from '@kbn/cases-plugin/server'; import type { MlFeatures } from '../../common/constants/app'; import { @@ -12,14 +13,29 @@ import { CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE, } from '../../common/constants/cases'; -export function registerCasesPersistableState(cases: CasesSetup, enabledFeatures: MlFeatures) { +export function registerCasesPersistableState( + cases: CasesSetup, + enabledFeatures: MlFeatures, + logger: Logger +) { if (enabledFeatures.ad === true) { - cases.attachmentFramework.registerPersistableState({ - id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE, - }); - - cases.attachmentFramework.registerPersistableState({ - id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS, - }); + try { + cases.attachmentFramework.registerPersistableState({ + id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE, + }); + } catch (error) { + logger.warn( + `ML failed to register cases persistable state for ${CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE}` + ); + } + try { + cases.attachmentFramework.registerPersistableState({ + id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS, + }); + } catch (error) { + logger.warn( + `ML failed to register cases persistable state for ${CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS}` + ); + } } } diff --git a/x-pack/plugins/ml/server/lib/register_sample_data_set_links.ts b/x-pack/plugins/ml/server/lib/register_sample_data_set_links.ts index 8f79966ac64db2..ec5709d09df38b 100644 --- a/x-pack/plugins/ml/server/lib/register_sample_data_set_links.ts +++ b/x-pack/plugins/ml/server/lib/register_sample_data_set_links.ts @@ -5,13 +5,15 @@ * 2.0. */ +import type { Logger } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; import type { HomeServerPluginSetup } from '@kbn/home-plugin/server'; import type { MlFeatures } from '../../common/constants/app'; export function registerSampleDataSetLinks( home: HomeServerPluginSetup, - enabledFeatures: MlFeatures + enabledFeatures: MlFeatures, + logger: Logger ) { if (enabledFeatures.ad === true) { const sampleDataLinkLabel = i18n.translate('xpack.ml.sampleDataLinkLabel', { @@ -21,28 +23,36 @@ export function registerSampleDataSetLinks( const getCreateJobPath = (jobId: string, dataViewId: string) => `/app/ml/modules/check_view_or_create?id=${jobId}&index=${dataViewId}`; - addAppLinksToSampleDataset('ecommerce', [ - { - sampleObject: { - type: 'index-pattern', - id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + try { + addAppLinksToSampleDataset('ecommerce', [ + { + sampleObject: { + type: 'index-pattern', + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + }, + getPath: (objectId) => getCreateJobPath('sample_data_ecommerce', objectId), + label: sampleDataLinkLabel, + icon: 'machineLearningApp', }, - getPath: (objectId) => getCreateJobPath('sample_data_ecommerce', objectId), - label: sampleDataLinkLabel, - icon: 'machineLearningApp', - }, - ]); + ]); + } catch (error) { + logger.warn(`ML failed to register sample data links for sample_data_ecommerce`); + } - addAppLinksToSampleDataset('logs', [ - { - sampleObject: { - type: 'index-pattern', - id: '90943e30-9a47-11e8-b64d-95841ca0b247', + try { + addAppLinksToSampleDataset('logs', [ + { + sampleObject: { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + }, + getPath: (objectId) => getCreateJobPath('sample_data_weblogs', objectId), + label: sampleDataLinkLabel, + icon: 'machineLearningApp', }, - getPath: (objectId) => getCreateJobPath('sample_data_weblogs', objectId), - label: sampleDataLinkLabel, - icon: 'machineLearningApp', - }, - ]); + ]); + } catch (error) { + logger.warn(`ML failed to register sample data links for sample_data_weblogs`); + } } } diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 962a9cff24b598..e0d40a477b7ed8 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -313,10 +313,10 @@ export class MlServerPlugin if (mlLicense.isMlEnabled() && mlLicense.isFullLicense()) { if (this.cases) { - registerCasesPersistableState(this.cases, this.enabledFeatures); + registerCasesPersistableState(this.cases, this.enabledFeatures, this.log); } if (this.home) { - registerSampleDataSetLinks(this.home, this.enabledFeatures); + registerSampleDataSetLinks(this.home, this.enabledFeatures, this.log); } } // check whether the job saved objects exist diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts index bb7ae7d04c418d..2f221b9a6d78dc 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts @@ -35,7 +35,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(types).to.eql({ '.lens': '78559fd806809ac3a1008942ead2a079864054f5', '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', - aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', }); }); }); From 1814580e1b11c8726daa995860de9ff6a1bf10c5 Mon Sep 17 00:00:00 2001 From: Jordan <51442161+JordanSh@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:28:04 +0300 Subject: [PATCH 04/24] [Cloud Security] Remove azure suffix from account type (#167746) --- .../components/fleet_extensions/policy_template_form.test.tsx | 4 ++-- .../components/fleet_extensions/policy_template_form.tsx | 4 ++-- .../fleet/public/components/agent_enrollment_flyout/types.ts | 4 +--- .../fleet/public/components/azure_arm_template_guide.tsx | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx index c0ad45a98f4f65..e9457cdee850aa 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx @@ -1210,7 +1210,7 @@ describe('', () => { let policy = getMockPolicyAzure(); policy = getPosturePolicy(policy, CLOUDBEAT_AZURE, { 'azure.credentials.type': { value: 'arm_template' }, - 'azure.account_type': { value: 'single-account-azure' }, + 'azure.account_type': { value: 'single-account' }, }); const { getByText } = render( @@ -1233,7 +1233,7 @@ describe('', () => { let policy = getMockPolicyAzure(); policy = getPosturePolicy(policy, CLOUDBEAT_AZURE, { 'azure.credentials.type': { value: 'arm_template' }, - 'azure.account_type': { value: 'single-account-azure' }, + 'azure.account_type': { value: 'single-account' }, }); render(); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx index 306cc6da445fdd..a75f933b9bd363 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx @@ -87,8 +87,8 @@ export const AWS_SINGLE_ACCOUNT = 'single-account'; export const AWS_ORGANIZATION_ACCOUNT = 'organization-account'; export const GCP_SINGLE_ACCOUNT = 'single-account'; export const GCP_ORGANIZATION_ACCOUNT = 'organization-account'; -export const AZURE_SINGLE_ACCOUNT = 'single-account-azure'; -export const AZURE_ORGANIZATION_ACCOUNT = 'organization-account-azure'; +export const AZURE_SINGLE_ACCOUNT = 'single-account'; +export const AZURE_ORGANIZATION_ACCOUNT = 'organization-account'; type AwsAccountType = typeof AWS_SINGLE_ACCOUNT | typeof AWS_ORGANIZATION_ACCOUNT; type AzureAccountType = typeof AZURE_SINGLE_ACCOUNT | typeof AZURE_ORGANIZATION_ACCOUNT; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts index abad38a0d74ae3..a7995eb47fab38 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts @@ -17,9 +17,7 @@ export type K8sMode = export type CloudSecurityIntegrationType = 'kspm' | 'vuln_mgmt' | 'cspm'; export type CloudSecurityIntegrationAwsAccountType = 'single-account' | 'organization-account'; -export type CloudSecurityIntegrationAzureAccountType = - | 'single-account-azure' - | 'organization-account-azure'; +export type CloudSecurityIntegrationAzureAccountType = 'single-account' | 'organization-account'; export type FlyoutMode = 'managed' | 'standalone'; export type SelectionType = 'tabs' | 'radio' | undefined; diff --git a/x-pack/plugins/fleet/public/components/azure_arm_template_guide.tsx b/x-pack/plugins/fleet/public/components/azure_arm_template_guide.tsx index cc8a741f997621..65419e5bee8d13 100644 --- a/x-pack/plugins/fleet/public/components/azure_arm_template_guide.tsx +++ b/x-pack/plugins/fleet/public/components/azure_arm_template_guide.tsx @@ -56,7 +56,7 @@ export const AzureArmTemplateGuide = ({

      - {azureAccountType === 'organization-account-azure' ? ( + {azureAccountType === 'organization-account' ? (
    1. Date: Tue, 3 Oct 2023 09:47:18 +0100 Subject: [PATCH 05/24] [ML] Initial serverless functional tests for ML (#167493) Add serverless functional tests for each project. **Search** Opens the trained models page and checks that there are 4 models listed. **Observability** Creates a AD job and then navigates to the AD jobs list and checks that the job is listed. **Security** Creates a AD job and then navigates to the AD jobs list and checks that the job is listed. Creates a DFA job and then navigates to the DFA jobs list and checks that the job is listed. Navigates to the trained models page and checks that there are no trained models listed. Also adds tests for each project to ensure that kibana search bar only lists the pages which are enabled. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../search_deep_links.ts | 2 +- .../test/functional/services/ml/common_api.ts | 1 + .../functional/services/ml/test_resources.ts | 6 +- .../services/ml/trained_models_table.ts | 15 ++++ .../services/deployment_agnostic_services.ts | 1 + .../functional/services/index.ts | 2 + .../functional/services/ml/index.ts | 23 +++++ .../services/ml/observability_navigation.ts | 24 ++++++ .../services/ml/security_navigation.ts | 32 +++++++ .../test_suites/observability/index.ts | 1 + .../ml/anomaly_detection_jobs_list.ts | 52 ++++++++++++ .../test_suites/observability/ml/index.ts | 15 ++++ .../observability/ml/search_bar_features.ts | 85 +++++++++++++++++++ .../functional/test_suites/search/index.ts | 2 + .../functional/test_suites/search/ml/index.ts | 15 ++++ .../search/ml/search_bar_features.ts | 85 +++++++++++++++++++ .../search/ml/trained_models_list.ts | 35 ++++++++ .../functional/test_suites/security/index.ts | 1 + .../ml/anomaly_detection_jobs_list.ts | 52 ++++++++++++ .../ml/data_frame_analytics_jobs_list.ts | 52 ++++++++++++ .../test_suites/security/ml/index.ts | 17 ++++ .../security/ml/search_bar_features.ts | 85 +++++++++++++++++++ .../security/ml/trained_models_list.ts | 37 ++++++++ 23 files changed, 636 insertions(+), 4 deletions(-) create mode 100644 x-pack/test_serverless/functional/services/ml/index.ts create mode 100644 x-pack/test_serverless/functional/services/ml/observability_navigation.ts create mode 100644 x-pack/test_serverless/functional/services/ml/security_navigation.ts create mode 100644 x-pack/test_serverless/functional/test_suites/observability/ml/anomaly_detection_jobs_list.ts create mode 100644 x-pack/test_serverless/functional/test_suites/observability/ml/index.ts create mode 100644 x-pack/test_serverless/functional/test_suites/observability/ml/search_bar_features.ts create mode 100644 x-pack/test_serverless/functional/test_suites/search/ml/index.ts create mode 100644 x-pack/test_serverless/functional/test_suites/search/ml/search_bar_features.ts create mode 100644 x-pack/test_serverless/functional/test_suites/search/ml/trained_models_list.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ml/index.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ml/search_bar_features.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts index ca48b8a2a4075d..539c42bf763ca7 100644 --- a/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts @@ -263,7 +263,7 @@ function createDeepLinks( }; }, - getDataComparisonDeepLink: (): AppDeepLink => { + getDataDriftDeepLink: (): AppDeepLink => { return { id: 'dataDrift', title: i18n.translate('xpack.ml.deepLink.dataDrift', { diff --git a/x-pack/test/functional/services/ml/common_api.ts b/x-pack/test/functional/services/ml/common_api.ts index 8a0ef258fe9636..378f94985f71a9 100644 --- a/x-pack/test/functional/services/ml/common_api.ts +++ b/x-pack/test/functional/services/ml/common_api.ts @@ -12,6 +12,7 @@ import type { FtrProviderContext } from '../../ftr_provider_context'; const COMMON_REQUEST_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', + 'x-elastic-internal-origin': 'Kibana', }; export type MlCommonAPI = ProvidedType; diff --git a/x-pack/test/functional/services/ml/test_resources.ts b/x-pack/test/functional/services/ml/test_resources.ts index a256c623adbe04..8771db60eabb68 100644 --- a/x-pack/test/functional/services/ml/test_resources.ts +++ b/x-pack/test/functional/services/ml/test_resources.ts @@ -60,9 +60,9 @@ export function MachineLearningTestResourcesProvider( objectType: SavedObjectType, space?: string ): Promise { - const response = await supertest.get( - `${space ? `/s/${space}` : ''}/api/saved_objects/${objectType}/${id}` - ); + const response = await supertest + .get(`${space ? `/s/${space}` : ''}/api/saved_objects/${objectType}/${id}`) + .set(getCommonRequestHeader('1')); return response.status === 200; }, diff --git a/x-pack/test/functional/services/ml/trained_models_table.ts b/x-pack/test/functional/services/ml/trained_models_table.ts index fd993fe93a0300..52f5beab5c2484 100644 --- a/x-pack/test/functional/services/ml/trained_models_table.ts +++ b/x-pack/test/functional/services/ml/trained_models_table.ts @@ -162,6 +162,21 @@ export function TrainedModelsTableProvider( ); } + public async assertTableIsPopulated() { + await this.waitForModelsToLoad(); + const rows = await this.parseModelsTable(); + expect(rows.length).to.not.eql(0, `Expected trained model row count to be '>0' (got '0')`); + } + + public async assertTableIsNotPopulated() { + await this.waitForModelsToLoad(); + const rows = await this.parseModelsTable(); + expect(rows.length).to.eql( + 0, + `Expected trained model row count to be '0' (got '${rows.length}')` + ); + } + public async assertModelCollapsedActionsButtonExists(modelId: string, expectedValue: boolean) { const actionsExists = await this.doesModelCollapsedActionsButtonExist(modelId); expect(actionsExists).to.eql( diff --git a/x-pack/test_serverless/functional/services/deployment_agnostic_services.ts b/x-pack/test_serverless/functional/services/deployment_agnostic_services.ts index 69cc869e9b8d9c..112c898eea3589 100644 --- a/x-pack/test_serverless/functional/services/deployment_agnostic_services.ts +++ b/x-pack/test_serverless/functional/services/deployment_agnostic_services.ts @@ -47,6 +47,7 @@ const deploymentAgnosticFunctionalServices = _.pick(functionalServices, [ 'listingTable', 'managementMenu', 'menuToggle', + 'ml', 'monacoEditor', 'pieChart', 'pipelineEditor', diff --git a/x-pack/test_serverless/functional/services/index.ts b/x-pack/test_serverless/functional/services/index.ts index afc34f147d012e..da2688b22b645a 100644 --- a/x-pack/test_serverless/functional/services/index.ts +++ b/x-pack/test_serverless/functional/services/index.ts @@ -13,6 +13,7 @@ import { SvlObltNavigationServiceProvider } from './svl_oblt_navigation'; import { SvlSearchNavigationServiceProvider } from './svl_search_navigation'; import { SvlSecNavigationServiceProvider } from './svl_sec_navigation'; import { SvlCommonScreenshotsProvider } from './svl_common_screenshots'; +import { MachineLearningProvider } from './ml'; export const services = { // deployment agnostic FTR services @@ -25,4 +26,5 @@ export const services = { svlSearchNavigation: SvlSearchNavigationServiceProvider, svlSecNavigation: SvlSecNavigationServiceProvider, svlCommonScreenshots: SvlCommonScreenshotsProvider, + svlMl: MachineLearningProvider, }; diff --git a/x-pack/test_serverless/functional/services/ml/index.ts b/x-pack/test_serverless/functional/services/ml/index.ts new file mode 100644 index 00000000000000..03cfc671b3a298 --- /dev/null +++ b/x-pack/test_serverless/functional/services/ml/index.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 { FtrProviderContext } from '../../ftr_provider_context'; + +import { MachineLearningNavigationProviderObservability } from './observability_navigation'; +import { MachineLearningNavigationProviderSecurity } from './security_navigation'; + +export function MachineLearningProvider(context: FtrProviderContext) { + const observabilityNavigation = MachineLearningNavigationProviderObservability(context); + const securityNavigation = MachineLearningNavigationProviderSecurity(context); + + return { + navigation: { + observability: observabilityNavigation, + security: securityNavigation, + }, + }; +} diff --git a/x-pack/test_serverless/functional/services/ml/observability_navigation.ts b/x-pack/test_serverless/functional/services/ml/observability_navigation.ts new file mode 100644 index 00000000000000..f09467263defff --- /dev/null +++ b/x-pack/test_serverless/functional/services/ml/observability_navigation.ts @@ -0,0 +1,24 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningNavigationProviderObservability({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + async function navigateToArea(id: string) { + await testSubjects.click('~nav-item-id-aiops'); + await testSubjects.existOrFail(`~nav-item-id-ml:${id}`, { timeout: 60 * 1000 }); + await testSubjects.click(`~nav-item-id-ml:${id}`); + } + + return { + async navigateToAnomalyDetection() { + await navigateToArea('anomalyDetection'); + }, + }; +} diff --git a/x-pack/test_serverless/functional/services/ml/security_navigation.ts b/x-pack/test_serverless/functional/services/ml/security_navigation.ts new file mode 100644 index 00000000000000..2d80ebd9d413e5 --- /dev/null +++ b/x-pack/test_serverless/functional/services/ml/security_navigation.ts @@ -0,0 +1,32 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningNavigationProviderSecurity({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + async function navigateToArea(id: string) { + await testSubjects.click('~solutionSideNavItemButton-machine_learning-landing'); + await testSubjects.existOrFail(`~solutionSideNavPanelLink-ml:${id}`, { + timeout: 60 * 1000, + }); + await testSubjects.click(`~solutionSideNavPanelLink-ml:${id}`); + } + + return { + async navigateToAnomalyDetection() { + await navigateToArea('anomalyDetection'); + }, + async navigateToDataFrameAnalytics() { + await navigateToArea('dataFrameAnalytics'); + }, + async navigateToTrainedModels() { + await navigateToArea('nodesOverview'); + }, + }; +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/index.ts b/x-pack/test_serverless/functional/test_suites/observability/index.ts index 3c387337a23e5f..cf624004cd3cb9 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/index.ts @@ -18,5 +18,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./cases/create_case_form')); loadTestFile(require.resolve('./cases/list_view')); loadTestFile(require.resolve('./advanced_settings')); + loadTestFile(require.resolve('./ml')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/observability/ml/anomaly_detection_jobs_list.ts b/x-pack/test_serverless/functional/test_suites/observability/ml/anomaly_detection_jobs_list.ts new file mode 100644 index 00000000000000..10f203889e1eaa --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/ml/anomaly_detection_jobs_list.ts @@ -0,0 +1,52 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const ml = getService('ml'); + const svlMl = getService('svlMl'); + const PageObjects = getPageObjects(['svlCommonPage']); + const adJobId = 'fq_single_permission'; + + describe('Anomaly detection jobs list', () => { + before(async () => { + await PageObjects.svlCommonPage.login(); + + await ml.api.createAnomalyDetectionJob(ml.commonConfig.getADFqMultiMetricJobConfig(adJobId)); + }); + + after(async () => { + await PageObjects.svlCommonPage.forceLogout(); + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + describe('page navigation', () => { + it('renders job list and finds created job', async () => { + await ml.navigation.navigateToMl(); + await ml.testExecution.logTestStep('loads the anomaly detection area'); + await svlMl.navigation.observability.navigateToAnomalyDetection(); + + await ml.testExecution.logTestStep('should display the stats bar and the AD job table'); + await ml.jobManagement.assertJobStatsBarExists(); + await ml.jobManagement.assertJobTableExists(); + + await ml.testExecution.logTestStep('should display an enabled "Create job" button'); + await ml.jobManagement.assertCreateNewJobButtonExists(); + await ml.jobManagement.assertCreateNewJobButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the AD job in the list'); + await ml.jobTable.filterWithSearchString(adJobId, 1); + + await ml.testExecution.logTestStep('should display enabled AD job result links'); + await ml.jobTable.assertJobActionSingleMetricViewerButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionAnomalyExplorerButtonEnabled(adJobId, true); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/ml/index.ts b/x-pack/test_serverless/functional/test_suites/observability/ml/index.ts new file mode 100644 index 00000000000000..e50d10b5ce6fdc --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/ml/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Observability ML', function () { + loadTestFile(require.resolve('./anomaly_detection_jobs_list')); + loadTestFile(require.resolve('./search_bar_features')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/ml/search_bar_features.ts b/x-pack/test_serverless/functional/test_suites/observability/ml/search_bar_features.ts new file mode 100644 index 00000000000000..d3899b6d45073e --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/ml/search_bar_features.ts @@ -0,0 +1,85 @@ +/* + * 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 '../../../ftr_provider_context'; + +export default function ({ getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['svlCommonPage', 'svlCommonNavigation']); + + const allLabels = [ + { label: 'Machine Learning', expected: true }, + { label: 'Machine Learning / Overview', expected: true }, + { label: 'Machine Learning / Anomaly Detection', expected: true }, + { label: 'Machine Learning / Anomaly Detection / Anomaly explorer', expected: true }, + { label: 'Machine Learning / Anomaly Detection / Single metric viewer', expected: true }, + { label: 'Machine Learning / Data Frame Analytics', expected: false }, + { label: 'Machine Learning / Data Frame Analytics / Results explorer', expected: false }, + { label: 'Machine Learning / Data Frame Analytics / Analytics map', expected: false }, + { label: 'Machine Learning / Model Management', expected: false }, + { label: 'Machine Learning / Model Management / Trained Models', expected: false }, + { label: 'Machine Learning / Model Management / Nodes', expected: false }, + { label: 'Machine Learning / Memory Usage', expected: true }, + { label: 'Machine Learning / Settings', expected: true }, + { label: 'Machine Learning / Settings / Calendars', expected: true }, + { label: 'Machine Learning / Settings / Filter Lists', expected: true }, + { label: 'Machine Learning / AIOps', expected: true }, + { label: 'Machine Learning / AIOps / Log Rate Analysis', expected: true }, + { label: 'Machine Learning / AIOps / Log Pattern Analysis', expected: true }, + { label: 'Machine Learning / AIOps / Change Point Detection', expected: true }, + { label: 'Machine Learning / Notifications', expected: true }, + { label: 'Machine Learning / Data Visualizer', expected: true }, + { label: 'Machine Learning / File Upload', expected: true }, + { label: 'Machine Learning / Index Data Visualizer', expected: true }, + { label: 'Machine Learning / Data Drift', expected: true }, + { label: 'Alerts and Insights / Machine Learning', expected: true }, + ]; + + describe('Search bar features', () => { + before(async () => { + await PageObjects.svlCommonPage.login(); + }); + + after(async () => { + await PageObjects.svlCommonPage.forceLogout(); + }); + + describe('list features', () => { + it('has the correct features enabled', async () => { + await PageObjects.svlCommonNavigation.search.showSearch(); + + const expectedLabels = allLabels.filter((l) => l.expected).map((l) => l.label); + + for (const expectedLabel of expectedLabels) { + await PageObjects.svlCommonNavigation.search.searchFor(expectedLabel); + const [result] = await PageObjects.svlCommonNavigation.search.getDisplayedResults(); + const label = result?.label; + expect(label).to.eql( + expectedLabel, + `First result should be ${expectedLabel} (got matching items '${label}')` + ); + } + await PageObjects.svlCommonNavigation.search.hideSearch(); + }); + it('has the correct features disabled', async () => { + await PageObjects.svlCommonNavigation.search.showSearch(); + + const notExpectedLabels = allLabels.filter((l) => !l.expected).map((l) => l.label); + + for (const notExpectedLabel of notExpectedLabels) { + await PageObjects.svlCommonNavigation.search.searchFor(notExpectedLabel); + const [result] = await PageObjects.svlCommonNavigation.search.getDisplayedResults(); + const label = result?.label; + expect(label).to.not.eql( + notExpectedLabel, + `First result should not be ${notExpectedLabel} (got matching items '${label}')` + ); + } + await PageObjects.svlCommonNavigation.search.hideSearch(); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/search/index.ts b/x-pack/test_serverless/functional/test_suites/search/index.ts index fbaaf96aed8a4d..6415d8cad90927 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index.ts @@ -17,5 +17,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboards/build_dashboard')); loadTestFile(require.resolve('./dashboards/import_dashboard')); loadTestFile(require.resolve('./advanced_settings')); + + loadTestFile(require.resolve('./ml')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/search/ml/index.ts b/x-pack/test_serverless/functional/test_suites/search/ml/index.ts new file mode 100644 index 00000000000000..1f3a48f89d7d61 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/ml/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Search ML', function () { + loadTestFile(require.resolve('./trained_models_list')); + loadTestFile(require.resolve('./search_bar_features')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/search/ml/search_bar_features.ts b/x-pack/test_serverless/functional/test_suites/search/ml/search_bar_features.ts new file mode 100644 index 00000000000000..f410b4c1668c6a --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/ml/search_bar_features.ts @@ -0,0 +1,85 @@ +/* + * 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 '../../../ftr_provider_context'; + +export default function ({ getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['svlCommonPage', 'svlCommonNavigation']); + + const allLabels = [ + { label: 'Machine Learning', expected: true }, + { label: 'Machine Learning / Overview', expected: false }, + { label: 'Machine Learning / Anomaly Detection', expected: false }, + { label: 'Machine Learning / Anomaly Detection / Anomaly explorer', expected: false }, + { label: 'Machine Learning / Anomaly Detection / Single metric viewer', expected: false }, + { label: 'Machine Learning / Data Frame Analytics', expected: false }, + { label: 'Machine Learning / Data Frame Analytics / Results explorer', expected: false }, + { label: 'Machine Learning / Data Frame Analytics / Analytics map', expected: false }, + { label: 'Machine Learning / Model Management', expected: true }, + { label: 'Machine Learning / Model Management / Trained Models', expected: true }, + { label: 'Machine Learning / Model Management / Nodes', expected: false }, + { label: 'Machine Learning / Memory Usage', expected: true }, + { label: 'Machine Learning / Settings', expected: false }, + { label: 'Machine Learning / Settings / Calendars', expected: false }, + { label: 'Machine Learning / Settings / Filter Lists', expected: false }, + { label: 'Machine Learning / AIOps', expected: true }, + { label: 'Machine Learning / AIOps / Log Rate Analysis', expected: true }, + { label: 'Machine Learning / AIOps / Log Pattern Analysis', expected: true }, + { label: 'Machine Learning / AIOps / Change Point Detection', expected: true }, + { label: 'Machine Learning / Notifications', expected: true }, + { label: 'Machine Learning / Data Visualizer', expected: true }, + { label: 'Machine Learning / File Upload', expected: true }, + { label: 'Machine Learning / Index Data Visualizer', expected: true }, + { label: 'Machine Learning / Data Drift', expected: true }, + { label: 'Alerts and Insights / Machine Learning', expected: true }, + ]; + + describe('Search bar features', () => { + before(async () => { + await PageObjects.svlCommonPage.login(); + }); + + after(async () => { + await PageObjects.svlCommonPage.forceLogout(); + }); + + describe('list features', () => { + it('has the correct features enabled', async () => { + await PageObjects.svlCommonNavigation.search.showSearch(); + + const expectedLabels = allLabels.filter((l) => l.expected).map((l) => l.label); + + for (const expectedLabel of expectedLabels) { + await PageObjects.svlCommonNavigation.search.searchFor(expectedLabel); + const [result] = await PageObjects.svlCommonNavigation.search.getDisplayedResults(); + const label = result?.label; + expect(label).to.eql( + expectedLabel, + `First result should be ${expectedLabel} (got matching items '${label}')` + ); + } + await PageObjects.svlCommonNavigation.search.hideSearch(); + }); + it('has the correct features disabled', async () => { + await PageObjects.svlCommonNavigation.search.showSearch(); + + const notExpectedLabels = allLabels.filter((l) => !l.expected).map((l) => l.label); + + for (const notExpectedLabel of notExpectedLabels) { + await PageObjects.svlCommonNavigation.search.searchFor(notExpectedLabel); + const [result] = await PageObjects.svlCommonNavigation.search.getDisplayedResults(); + const label = result?.label; + expect(label).to.not.eql( + notExpectedLabel, + `First result should not be ${notExpectedLabel} (got matching items '${label}')` + ); + } + await PageObjects.svlCommonNavigation.search.hideSearch(); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/search/ml/trained_models_list.ts b/x-pack/test_serverless/functional/test_suites/search/ml/trained_models_list.ts new file mode 100644 index 00000000000000..22adf85dc39269 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/ml/trained_models_list.ts @@ -0,0 +1,35 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const ml = getService('ml'); + const PageObjects = getPageObjects(['svlCommonPage']); + + describe('Trained models list', () => { + before(async () => { + await PageObjects.svlCommonPage.login(); + }); + + after(async () => { + await PageObjects.svlCommonPage.forceLogout(); + }); + + describe('page navigation', () => { + it('renders trained models list', async () => { + await ml.navigation.navigateToMl(); + await ml.testExecution.logTestStep('should load the trained models page'); + + await ml.testExecution.logTestStep( + 'should display the stats bar and the analytics table with 1 installed trained model and built in elser models in the table' + ); + await ml.trainedModels.assertStats(1); + await ml.trainedModelsTable.assertTableIsPopulated(); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/index.ts b/x-pack/test_serverless/functional/test_suites/security/index.ts index d68c184813ceaf..8525b10e9b91a6 100644 --- a/x-pack/test_serverless/functional/test_suites/security/index.ts +++ b/x-pack/test_serverless/functional/test_suites/security/index.ts @@ -18,5 +18,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./ftr/cases/configure')); loadTestFile(require.resolve('./ftr/cases/list_view')); loadTestFile(require.resolve('./advanced_settings')); + loadTestFile(require.resolve('./ml')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts b/x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts new file mode 100644 index 00000000000000..2e60830d9f8602 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ml/anomaly_detection_jobs_list.ts @@ -0,0 +1,52 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const ml = getService('ml'); + const svlMl = getService('svlMl'); + const PageObjects = getPageObjects(['svlCommonPage']); + const adJobId = 'fq_single_permission'; + + describe('Anomaly detection jobs list', () => { + before(async () => { + await PageObjects.svlCommonPage.login(); + + await ml.api.createAnomalyDetectionJob(ml.commonConfig.getADFqMultiMetricJobConfig(adJobId)); + }); + + after(async () => { + await PageObjects.svlCommonPage.forceLogout(); + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + describe('page navigation', () => { + it('renders job list and finds created job', async () => { + await ml.navigation.navigateToMl(); + + await ml.testExecution.logTestStep('loads the anomaly detection area'); + await svlMl.navigation.security.navigateToAnomalyDetection(); + + await ml.testExecution.logTestStep('should display the stats bar and the AD job table'); + await ml.jobManagement.assertJobStatsBarExists(); + await ml.jobManagement.assertJobTableExists(); + + await ml.testExecution.logTestStep('should display an enabled "Create job" button'); + await ml.jobManagement.assertCreateNewJobButtonExists(); + await ml.jobManagement.assertCreateNewJobButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the AD job in the list'); + await ml.jobTable.filterWithSearchString(adJobId, 1); + + await ml.testExecution.logTestStep('should display enabled AD job result links'); + await ml.jobTable.assertJobActionSingleMetricViewerButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionAnomalyExplorerButtonEnabled(adJobId, true); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts b/x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts new file mode 100644 index 00000000000000..8cd6f2a5708e2a --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ml/data_frame_analytics_jobs_list.ts @@ -0,0 +1,52 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const svlMl = getService('svlMl'); + const PageObjects = getPageObjects(['svlCommonPage']); + const dfaJobId = 'iph_outlier_permission'; + + describe('Data frame analytics jobs list', () => { + before(async () => { + await PageObjects.svlCommonPage.login(); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); + + await ml.api.createDataFrameAnalyticsJob( + ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(dfaJobId) + ); + }); + + after(async () => { + await PageObjects.svlCommonPage.forceLogout(); + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + describe('page navigation', () => { + it('renders job list and finds created job', async () => { + await ml.testExecution.logTestStep('should load the DFA job management page'); + await svlMl.navigation.security.navigateToDataFrameAnalytics(); + + await ml.testExecution.logTestStep('should display the stats bar and the analytics table'); + await ml.dataFrameAnalytics.assertAnalyticsStatsBarExists(); + await ml.dataFrameAnalytics.assertAnalyticsTableExists(); + + await ml.testExecution.logTestStep('should display an enabled "Create job" button'); + await ml.dataFrameAnalytics.assertCreateNewAnalyticsButtonExists(); + await ml.dataFrameAnalytics.assertCreateNewAnalyticsButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the DFA job in the list'); + await ml.dataFrameAnalyticsTable.filterWithSearchString(dfaJobId, 1); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/index.ts b/x-pack/test_serverless/functional/test_suites/security/ml/index.ts new file mode 100644 index 00000000000000..5b486161646dfb --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ml/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Security ML', function () { + loadTestFile(require.resolve('./anomaly_detection_jobs_list')); + loadTestFile(require.resolve('./data_frame_analytics_jobs_list')); + loadTestFile(require.resolve('./trained_models_list')); + loadTestFile(require.resolve('./search_bar_features')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/search_bar_features.ts b/x-pack/test_serverless/functional/test_suites/security/ml/search_bar_features.ts new file mode 100644 index 00000000000000..a8571be2daeda3 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ml/search_bar_features.ts @@ -0,0 +1,85 @@ +/* + * 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 '../../../ftr_provider_context'; + +export default function ({ getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['svlCommonPage', 'svlCommonNavigation']); + + const allLabels = [ + { label: 'Machine Learning', expected: true }, + { label: 'Machine Learning / Overview', expected: true }, + { label: 'Machine Learning / Anomaly Detection', expected: true }, + { label: 'Machine Learning / Anomaly Detection / Anomaly explorer', expected: true }, + { label: 'Machine Learning / Anomaly Detection / Single metric viewer', expected: true }, + { label: 'Machine Learning / Data Frame Analytics', expected: true }, + { label: 'Machine Learning / Data Frame Analytics / Results explorer', expected: true }, + { label: 'Machine Learning / Data Frame Analytics / Analytics map', expected: true }, + { label: 'Machine Learning / Model Management', expected: true }, + { label: 'Machine Learning / Model Management / Trained Models', expected: true }, + { label: 'Machine Learning / Model Management / Nodes', expected: false }, + { label: 'Machine Learning / Memory Usage', expected: true }, + { label: 'Machine Learning / Settings', expected: true }, + { label: 'Machine Learning / Settings / Calendars', expected: true }, + { label: 'Machine Learning / Settings / Filter Lists', expected: true }, + { label: 'Machine Learning / AIOps', expected: true }, + { label: 'Machine Learning / AIOps / Log Rate Analysis', expected: true }, + { label: 'Machine Learning / AIOps / Log Pattern Analysis', expected: true }, + { label: 'Machine Learning / AIOps / Change Point Detection', expected: true }, + { label: 'Machine Learning / Notifications', expected: true }, + { label: 'Machine Learning / Data Visualizer', expected: true }, + { label: 'Machine Learning / File Upload', expected: true }, + { label: 'Machine Learning / Index Data Visualizer', expected: true }, + { label: 'Machine Learning / Data Drift', expected: true }, + { label: 'Alerts and Insights / Machine Learning', expected: true }, + ]; + + describe('Search bar features', () => { + before(async () => { + await PageObjects.svlCommonPage.login(); + }); + + after(async () => { + await PageObjects.svlCommonPage.forceLogout(); + }); + + describe('list features', () => { + it('has the correct features enabled', async () => { + await PageObjects.svlCommonNavigation.search.showSearch(); + + const expectedLabels = allLabels.filter((l) => l.expected).map((l) => l.label); + + for (const expectedLabel of expectedLabels) { + await PageObjects.svlCommonNavigation.search.searchFor(expectedLabel); + const [result] = await PageObjects.svlCommonNavigation.search.getDisplayedResults(); + const label = result?.label; + expect(label).to.eql( + expectedLabel, + `First result should be ${expectedLabel} (got matching items '${label}')` + ); + } + await PageObjects.svlCommonNavigation.search.hideSearch(); + }); + it('has the correct features disabled', async () => { + await PageObjects.svlCommonNavigation.search.showSearch(); + + const notExpectedLabels = allLabels.filter((l) => !l.expected).map((l) => l.label); + + for (const notExpectedLabel of notExpectedLabels) { + await PageObjects.svlCommonNavigation.search.searchFor(notExpectedLabel); + const [result] = await PageObjects.svlCommonNavigation.search.getDisplayedResults(); + const label = result?.label; + expect(label).to.not.eql( + notExpectedLabel, + `First result should not be ${notExpectedLabel} (got matching items '${label}')` + ); + } + await PageObjects.svlCommonNavigation.search.hideSearch(); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts b/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts new file mode 100644 index 00000000000000..19b1430dda1ffa --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts @@ -0,0 +1,37 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const ml = getService('ml'); + const svlMl = getService('svlMl'); + const PageObjects = getPageObjects(['svlCommonPage']); + + describe('Trained models list', () => { + before(async () => { + await PageObjects.svlCommonPage.login(); + }); + + after(async () => { + await PageObjects.svlCommonPage.forceLogout(); + }); + + describe('page navigation', () => { + it('renders trained models list', async () => { + await ml.navigation.navigateToMl(); + await ml.testExecution.logTestStep('should load the trained models page'); + await svlMl.navigation.security.navigateToTrainedModels(); + + await ml.testExecution.logTestStep( + 'should display the stats bar and the analytics table with no trained models' + ); + await ml.trainedModels.assertStats(0); + await ml.trainedModelsTable.assertTableIsNotPopulated(); + }); + }); + }); +} From d655a918b850e76e06a2db57059054fee9b326ca Mon Sep 17 00:00:00 2001 From: Wafaa Nasr Date: Tue, 3 Oct 2023 11:04:47 +0200 Subject: [PATCH 06/24] [Security Solution] [Exceptions] Fix selection `Add to rule` radio button from Alert table (#166611) ## Summary - Fixes selecting the `Add to rule` radio button selection when Exception opens from Alert. **Bug:** https://github.com/elastic/kibana/assets/12671903/69a15569-f194-4cf9-ae4e-bb41a5a652e1 --- .../components/add_exception_flyout/index.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx index 15880fcd423fca..9a50076530983c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx @@ -130,6 +130,12 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ } }, [rules]); + const addExceptionToRuleOrListSelection = useMemo(() => { + if (isBulkAction) return 'add_to_rules'; + if (rules?.length === 1 || isAlertDataLoading !== undefined) return 'add_to_rule'; + return 'select_rules_to_add_to'; + }, [isAlertDataLoading, isBulkAction, rules]); + const getListType = useMemo(() => { if (isEndpointItem) return ExceptionListTypeEnum.ENDPOINT; if (sharedListToAddTo) return ExceptionListTypeEnum.DETECTION; @@ -159,14 +165,11 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ dispatch, ] = useReducer(createExceptionItemsReducer(), { ...initialState, - addExceptionToRadioSelection: isBulkAction - ? 'add_to_rules' - : rules != null && rules.length === 1 - ? 'add_to_rule' - : 'select_rules_to_add_to', + addExceptionToRadioSelection: addExceptionToRuleOrListSelection, listType: getListType, selectedRulesToAddTo: rules != null ? rules : [], }); + const hasAlertData = useMemo((): boolean => { return alertData != null; }, [alertData]); From 62f17db60c4f1e41a4733768087c17da384dbd9f Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Tue, 3 Oct 2023 11:09:04 +0200 Subject: [PATCH 07/24] [SecuritySolutions] Fix delay when enabling risk engine (#166846) ## Summary Fix delay when enabling risk engine * Show the loading spinner while the status is fetching. * Extract a component from the main render due to high cyclomatic complexity **BEFORE** The toggle is off when the loader disappears and some moments later it toggles on. ![enable risk score delay](https://github.com/elastic/kibana/assets/1490444/411eec39-a928-4ffb-a66e-f45f87e07bc3) AFTER The toggle is on when the loader disappears. ![enable risk score delay fixed](https://github.com/elastic/kibana/assets/1490444/763727f2-34d1-4fdf-ba96-05afe8532c5a) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/risk_score_enable_section.tsx | 136 ++++++++++-------- 1 file changed, 77 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx index 4d32f11ed83dc3..0e48603421406e 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx @@ -82,9 +82,77 @@ const RiskScoreErrorPanel = ({ errors }: { errors: string[] }) => ( ); +interface RiskScoreUpdateModalParams { + isLoading: boolean; + isVisible: boolean; + closeModal: () => void; + onConfirm: () => void; +} + +const RiskScoreUpdateModal = ({ + closeModal, + isLoading, + onConfirm, + isVisible, +}: RiskScoreUpdateModalParams) => { + if (!isVisible) return null; + + return ( + + {isLoading ? ( + + + + {i18n.UPDATING_RISK_ENGINE} + + + ) : ( + <> + + {i18n.UPDATE_RISK_ENGINE_MODAL_TITLE} + + + + +

      + {i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_1} + {i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_2} +

      + +

      + {i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_1} + {i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_2} +

      +
      + +
      + + + + {i18n.UPDATE_RISK_ENGINE_MODAL_BUTTON_NO} + + + {i18n.UPDATE_RISK_ENGINE_MODAL_BUTTON_YES} + + + + )} +
      + ); +}; + export const RiskScoreEnableSection = () => { const [isModalVisible, setIsModalVisible] = useState(false); - const { data: riskEngineStatus } = useRiskEngineStatus(); + const { data: riskEngineStatus, isFetching: isStatusLoading } = useRiskEngineStatus(); const initRiskEngineMutation = useInitRiskEngineMutation({ onSettled: () => { setIsModalVisible(false); @@ -102,7 +170,8 @@ export const RiskScoreEnableSection = () => { const isLoading = initRiskEngineMutation.isLoading || enableRiskEngineMutation.isLoading || - disableRiskEngineMutation.isLoading; + disableRiskEngineMutation.isLoading || + isStatusLoading; const isUpdateAvailable = riskEngineStatus?.isUpdateAvailable; const btnIsDisabled = !currentRiskEngineStatus || isLoading; @@ -121,62 +190,6 @@ export const RiskScoreEnableSection = () => { } }; - let modal; - - if (isModalVisible) { - modal = ( - - {initRiskEngineMutation.isLoading ? ( - - - - {i18n.UPDATING_RISK_ENGINE} - - - ) : ( - <> - - {i18n.UPDATE_RISK_ENGINE_MODAL_TITLE} - - - - -

      - {i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_1} - {i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_2} -

      - -

      - {i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_1} - {i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_2} -

      -
      - -
      - - - - {i18n.UPDATE_RISK_ENGINE_MODAL_BUTTON_NO} - - initRiskEngineMutation.mutate()} - fill - > - {i18n.UPDATE_RISK_ENGINE_MODAL_BUTTON_YES} - - - - )} -
      - ); - } - let initRiskEngineErrors: string[] = []; if (initRiskEngineMutation.isError) { @@ -219,7 +232,12 @@ export const RiskScoreEnableSection = () => { - {modal} + initRiskEngineMutation.mutate()} + isLoading={initRiskEngineMutation.isLoading} + closeModal={closeModal} + /> From 084362f6b35e4d68cecc042d7e96b26d55fa8087 Mon Sep 17 00:00:00 2001 From: Jordan <51442161+JordanSh@users.noreply.github.com> Date: Tue, 3 Oct 2023 12:13:09 +0300 Subject: [PATCH 08/24] [Cloud Security] Vulnerability dashboard api testings (#162217) --- .../vulnerabilities_dashboard.ts | 2 +- .../test/cloud_security_posture_api/config.ts | 7 +- .../mocks/vulnerabilities_latest_mock.ts | 261 +++++++++++++++ .../routes/vulnerabilities_dashboard.ts | 311 ++++++++++++++++++ .../config.ts | 2 +- .../mocks/vulnerabilities_latest_mock.ts | 259 +++++++++++++++ .../page_objects/csp_dashboard_page.ts | 5 - .../page_objects/findings_page.ts | 5 + .../page_objects/index.ts | 2 + .../vulnerability_dashboard_page_object.ts | 120 +++++++ .../pages/index.ts | 1 + .../pages/vulnerability_dashboard.ts | 95 ++++++ x-pack/test/tsconfig.json | 2 + 13 files changed, 1063 insertions(+), 9 deletions(-) create mode 100644 x-pack/test/cloud_security_posture_api/routes/mocks/vulnerabilities_latest_mock.ts create mode 100644 x-pack/test/cloud_security_posture_api/routes/vulnerabilities_dashboard.ts create mode 100644 x-pack/test/cloud_security_posture_functional/mocks/vulnerabilities_latest_mock.ts create mode 100644 x-pack/test/cloud_security_posture_functional/page_objects/vulnerability_dashboard_page_object.ts create mode 100644 x-pack/test/cloud_security_posture_functional/pages/vulnerability_dashboard.ts diff --git a/x-pack/plugins/cloud_security_posture/server/routes/vulnerabilities_dashboard/vulnerabilities_dashboard.ts b/x-pack/plugins/cloud_security_posture/server/routes/vulnerabilities_dashboard/vulnerabilities_dashboard.ts index e77851062217c5..7f676226559aaf 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/vulnerabilities_dashboard/vulnerabilities_dashboard.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/vulnerabilities_dashboard/vulnerabilities_dashboard.ts @@ -61,7 +61,7 @@ export const defineGetVulnerabilitiesDashboardRoute = (router: CspRouter): void return response.customError({ body: { message: error.message }, - statusCode: error.statusCode, + statusCode: 500, }); } } diff --git a/x-pack/test/cloud_security_posture_api/config.ts b/x-pack/test/cloud_security_posture_api/config.ts index 8d210835d726cf..a206fd563cc00b 100644 --- a/x-pack/test/cloud_security_posture_api/config.ts +++ b/x-pack/test/cloud_security_posture_api/config.ts @@ -14,7 +14,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...xpackFunctionalConfig.getAll(), - testFiles: [require.resolve('./telemetry/telemetry.ts')], + testFiles: [ + require.resolve('./telemetry/telemetry.ts'), + require.resolve('./routes/vulnerabilities_dashboard.ts'), + ], junit: { reportName: 'X-Pack Cloud Security Posture API Tests', }, @@ -36,7 +39,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { * 2. merge the updated version number change to kibana */ `--xpack.fleet.packages.0.name=cloud_security_posture`, - `--xpack.fleet.packages.0.version=1.2.8`, + `--xpack.fleet.packages.0.version=1.5.0`, // `--xpack.fleet.registryUrl=https://localhost:8080`, ], }, diff --git a/x-pack/test/cloud_security_posture_api/routes/mocks/vulnerabilities_latest_mock.ts b/x-pack/test/cloud_security_posture_api/routes/mocks/vulnerabilities_latest_mock.ts new file mode 100644 index 00000000000000..3f5eb1e3ff76fb --- /dev/null +++ b/x-pack/test/cloud_security_posture_api/routes/mocks/vulnerabilities_latest_mock.ts @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const vulnerabilitiesLatestMock = [ + { + agent: { + name: 'ip-172-31-17-52', + id: '9e69d889-c0c0-4c3e-9dc7-01dcd7c7db10', + ephemeral_id: '4fce6ed8-c9b3-40bb-b805-77e884cce0ef', + type: 'cloudbeat', + version: '8.8.0', + }, + package: { + path: 'snap-07b9e0d5eb5f324a1 (amazon 2 (Karoo))', + fixed_version: '2.56.1-9.amzn2.0.6', + name: 'glib2', + type: 'amazon', + version: '2.56.1-9.amzn2.0.5', + }, + resource: { + name: 'name-ng-1-Node', + id: '02d62a7df23951b19', + }, + elastic_agent: { + id: '9e69d889-c0c0-4c3e-9dc7-01dcd7c7db10', + version: '8.8.0', + snapshot: false, + }, + vulnerability: { + severity: 'MEDIUM', + package: { + fixed_version: '2.56.1-9.amzn2.0.6', + name: 'glib2', + version: '2.56.1-9.amzn2.0.5', + }, + description: + 'PCRE before 8.38 mishandles the [: and \\\\ substrings in character classes, which allows remote attackers to cause a denial of service (uninitialized memory read) or possibly have unspecified other impact via a crafted regular expression, as demonstrated by a JavaScript RegExp object encountered by Konqueror.', + title: + 'pcre: uninitialized memory read triggered by malformed posix character class (8.38/22)', + classification: 'CVSS', + data_source: { + ID: 'amazon', + URL: 'https://alas.aws.amazon.com/', + Name: 'Amazon Linux Security Center', + }, + cwe: ['CWE-908'], + reference: 'https://avd.aquasec.com/nvd/cve-2015-8390', + score: { + version: '3.1', + base: 9.8, + }, + report_id: 1687955586, + scanner: { + vendor: 'Trivy', + version: 'v0.35.0', + }, + id: 'CVE-2015-8390', + enumeration: 'CVE', + cvss: { + redhat: { + V2Vector: 'AV:N/AC:M/Au:N/C:N/I:N/A:P', + V2Score: 4.3, + }, + nvd: { + V3Vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H', + V2Vector: 'AV:N/AC:L/Au:N/C:P/I:P/A:P', + V3Score: 9.8, + V2Score: 7.5, + }, + }, + class: 'os-pkgs', + published_date: '2015-12-02T01:59:00Z', + }, + cloud: { + provider: 'aws', + region: 'eu-west-1', + account: { + name: 'elastic-security-cloud-security-dev', + id: '704479110758', + }, + }, + '@timestamp': '2023-06-29T02:08:44.993Z', + cloudbeat: { + commit_sha: '4d990caa0c9c1594441da6bf24a685599aeb2bd5', + commit_time: '2023-05-15T14:48:10Z', + version: '8.8.0', + }, + ecs: { + version: '8.6.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'cloud_security_posture.vulnerabilities', + }, + host: { + name: 'ip-172-31-17-52', + }, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 1687955586, + ingested: '2023-07-13T09:55:39Z', + kind: 'state', + created: '2023-06-29T02:08:44.993386561Z', + id: '80d05bca-9900-4038-ac8d-bcefaf6afd0c', + type: ['info'], + category: ['vulnerability'], + dataset: 'cloud_security_posture.vulnerabilities', + outcome: 'success', + }, + }, + { + agent: { + name: 'ip-172-31-17-52', + id: '9e69d889-c0c0-4c3e-9dc7-01dcd7c7db11', + type: 'cloudbeat', + ephemeral_id: '4fce6ed8-c9b3-40bb-b805-77e884cce0ef', + version: '8.8.0', + }, + package: { + path: 'snap-08c227d5c8a3dc1f2 (amazon 2 (Karoo))', + fixed_version: '2.56.1-9.amzn2.0.6', + name: 'glib2', + type: 'amazon', + version: '2.56.1-9.amzn2.0.5', + }, + resource: { + name: 'othername-june12-8-8-0-1', + id: '09d11277683ea41c5', + }, + elastic_agent: { + id: '9e69d889-c0c0-4c3e-9dc7-01dcd7c7db11', + version: '8.8.0', + snapshot: false, + }, + vulnerability: { + severity: 'HIGH', + package: { + fixed_version: '2.56.1-9.amzn2.0.6', + name: 'glib2', + version: '2.56.1-9.amzn2.0.5', + }, + description: + 'PCRE before 8.38 mishandles the (?() and (?(R) conditions, which allows remote attackers to cause a denial of service (integer overflow) or possibly have unspecified other impact via a crafted regular expression, as demonstrated by a JavaScript RegExp object encountered by Konqueror.', + classification: 'CVSS', + title: 'pcre: Integer overflow caused by missing check for certain conditions (8.38/31)', + data_source: { + ID: 'amazon', + URL: 'https://alas.aws.amazon.com/', + Name: 'Amazon Linux Security Center', + }, + reference: 'https://avd.aquasec.com/nvd/cve-2015-8394', + cwe: ['CWE-190'], + score: { + version: '3.1', + base: 9.8, + }, + report_id: 1687955586, + scanner: { + vendor: 'Trivy', + version: 'v0.35.0', + }, + id: 'CVE-2015-8394', + enumeration: 'CVE', + class: 'os-pkgs', + published_date: '2015-12-02T01:59:00Z', + cvss: { + redhat: { + V2Vector: 'AV:N/AC:M/Au:N/C:N/I:N/A:P', + V2Score: 4.3, + }, + nvd: { + V3Vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H', + V2Vector: 'AV:N/AC:L/Au:N/C:P/I:P/A:P', + V3Score: 9.8, + V2Score: 7.5, + }, + }, + }, + cloud: { + provider: 'aws', + region: 'eu-west-1', + account: { + name: 'elastic-security-cloud-security-dev', + id: '704479110758', + }, + }, + '@timestamp': '2023-06-29T02:08:16.535Z', + ecs: { + version: '8.6.0', + }, + cloudbeat: { + commit_sha: '4d990caa0c9c1594441da6bf24a685599aeb2bd5', + commit_time: '2023-05-15T14:48:10Z', + version: '8.8.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'cloud_security_posture.vulnerabilities', + }, + host: { + name: 'ip-172-31-17-52', + }, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 1687955586, + ingested: '2023-07-13T09:55:39Z', + created: '2023-06-29T02:08:16.535506246Z', + kind: 'state', + id: '7cd4309a-01dd-433c-8c44-14019f8b1522', + category: ['vulnerability'], + type: ['info'], + dataset: 'cloud_security_posture.vulnerabilities', + outcome: 'success', + }, + }, +]; + +export const scoresVulnerabilitiesMock = [ + { + '@timestamp': '2023-09-03T11:36:58.441344Z', + critical: 0, + high: 1, + medium: 1, + low: 0, + policy_template: 'vuln_mgmt', + vulnerabilities_stats_by_cloud_account: { + '704479110758': { + cloudAccountName: 'elastic-security-cloud-security-dev', + cloudAccountId: '704479110758', + critical: 0, + high: 1, + medium: 1, + low: 0, + }, + }, + }, + { + '@timestamp': '2023-09-03T11:36:58.441344Z', + critical: 0, + high: 1, + medium: 1, + low: 0, + policy_template: 'vuln_mgmt', + vulnerabilities_stats_by_cloud_account: { + '704479110758': { + cloudAccountName: 'elastic-security-cloud-security-dev', + cloudAccountId: '704479110758', + critical: 0, + high: 1, + medium: 1, + low: 0, + }, + }, + }, +]; diff --git a/x-pack/test/cloud_security_posture_api/routes/vulnerabilities_dashboard.ts b/x-pack/test/cloud_security_posture_api/routes/vulnerabilities_dashboard.ts new file mode 100644 index 00000000000000..7f15daf653478f --- /dev/null +++ b/x-pack/test/cloud_security_posture_api/routes/vulnerabilities_dashboard.ts @@ -0,0 +1,311 @@ +/* + * 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 { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { EcsEvent } from '@kbn/ecs'; +import type { FtrProviderContext } from '../ftr_provider_context'; +import { + vulnerabilitiesLatestMock, + scoresVulnerabilitiesMock, +} from './mocks/vulnerabilities_latest_mock'; + +export interface CnvmStatistics { + criticalCount?: number; + highCount?: number; + mediumCount?: number; + resourcesScanned?: number; + cloudRegions?: number; +} + +export interface AccountVulnStats { + cloudAccountId: string; + cloudAccountName: string; + critical?: number; + high?: number; + medium?: number; + low?: number; +} + +export interface VulnStatsTrend { + '@timestamp': string; + policy_template: 'vuln_mgmt'; + critical: number; + high: number; + medium: number; + low: number; + vulnerabilities_stats_by_cloud_account?: Record< + AccountVulnStats['cloudAccountId'], + AccountVulnStats + >; + event: EcsEvent; +} + +export interface VulnerableResourceStat { + vulnerabilityCount?: number; + resource: { + id?: string; + name?: string; + }; + cloudRegion?: string; +} + +export interface PatchableVulnerabilityStat { + vulnerabilityCount?: number; + packageFixVersion?: string; + cve?: string; + cvss: { + score?: number; + version?: string; + }; +} + +export interface VulnerabilityStat { + packageFixVersion?: string; + packageName?: string; + packageVersion?: string; + severity?: string; + vulnerabilityCount?: number; + cvss: { + score?: number; + version?: string; + }; +} + +export interface CnvmDashboardData { + cnvmStatistics: CnvmStatistics; + vulnTrends: VulnStatsTrend[]; + topVulnerableResources: VulnerableResourceStat[]; + topPatchableVulnerabilities: PatchableVulnerabilityStat[]; + topVulnerabilities: VulnerabilityStat[]; +} + +const VULNERABILITIES_LATEST_INDEX = 'logs-cloud_security_posture.vulnerabilities_latest-default'; +const BENCHMARK_SCORES_INDEX = 'logs-cloud_security_posture.scores-default'; + +type CnvmDashboardDataWithoutTimestamp = Omit & { + vulnTrends: Array>; +}; + +const removeRealtimeCalculatedFields = ( + responseBody: CnvmDashboardData +): CnvmDashboardDataWithoutTimestamp => { + const cleanedVulnTrends = responseBody.vulnTrends.map((trend) => { + const { ['@timestamp']: timestamp, event, ...rest } = trend; + return rest; + }); + + return { + ...responseBody, + vulnTrends: cleanedVulnTrends, + }; +}; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const retry = getService('retry'); + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + const index = { + addFindings: async (vulnerabilitiesMock: T[]) => { + await Promise.all( + vulnerabilitiesMock.map((vulnerabilityDoc) => + es.index({ + index: VULNERABILITIES_LATEST_INDEX, + body: vulnerabilityDoc, + refresh: true, + }) + ) + ); + }, + + addScores: async (scoresMock: T[]) => { + await Promise.all( + scoresMock.map((scoreDoc) => + es.index({ + index: BENCHMARK_SCORES_INDEX, + body: scoreDoc, + refresh: true, + }) + ) + ); + }, + + removeFindings: async () => { + const indexExists = await es.indices.exists({ index: VULNERABILITIES_LATEST_INDEX }); + + if (indexExists) { + es.deleteByQuery({ + index: VULNERABILITIES_LATEST_INDEX, + query: { match_all: {} }, + refresh: true, + }); + } + }, + + removeScores: async () => { + const indexExists = await es.indices.exists({ index: BENCHMARK_SCORES_INDEX }); + + if (indexExists) { + es.deleteByQuery({ + index: BENCHMARK_SCORES_INDEX, + query: { match_all: {} }, + refresh: true, + }); + } + }, + + deleteFindingsIndex: async () => { + const indexExists = await es.indices.exists({ index: VULNERABILITIES_LATEST_INDEX }); + + if (indexExists) { + await es.indices.delete({ index: VULNERABILITIES_LATEST_INDEX }); + } + }, + }; + + describe('Vulnerability Dashboard API', async () => { + beforeEach(async () => { + await waitForPluginInitialized(); + await index.addScores(scoresVulnerabilitiesMock); + await index.addFindings(vulnerabilitiesLatestMock); + }); + + afterEach(async () => { + await index.removeFindings(); + await index.removeScores(); + }); + + it('responds with a 200 status code and matching data mock', async () => { + const { body } = await supertest + .get(`/internal/cloud_security_posture/vulnerabilities_dashboard`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .expect(200); + + // @timestamp and event are real time calculated fields, we need to remove them in order to remove inconsistencies between mock and actual result + const cleanedBody = removeRealtimeCalculatedFields(body); + + expect(cleanedBody).to.eql({ + cnvmStatistics: { + criticalCount: 0, + highCount: 1, + mediumCount: 1, + resourcesScanned: 2, + cloudRegions: 1, + }, + vulnTrends: [ + { + high: 1, + policy_template: 'vuln_mgmt', + critical: 0, + low: 0, + vulnerabilities_stats_by_cloud_account: { + '704479110758': { + cloudAccountName: 'elastic-security-cloud-security-dev', + high: 1, + critical: 0, + low: 0, + cloudAccountId: '704479110758', + medium: 1, + }, + }, + medium: 1, + }, + ], + topVulnerableResources: [ + { + resource: { + id: '02d62a7df23951b19', + name: 'name-ng-1-Node', + }, + vulnerabilityCount: 1, + cloudRegion: 'eu-west-1', + }, + { + resource: { + id: '09d11277683ea41c5', + name: 'othername-june12-8-8-0-1', + }, + vulnerabilityCount: 1, + cloudRegion: 'eu-west-1', + }, + ], + topPatchableVulnerabilities: [ + { + cve: 'CVE-2015-8390', + cvss: { + score: 9.800000190734863, + version: '3.1', + }, + packageFixVersion: '2.56.1-9.amzn2.0.6', + vulnerabilityCount: 1, + }, + { + cve: 'CVE-2015-8394', + cvss: { + score: 9.800000190734863, + version: '3.1', + }, + packageFixVersion: '2.56.1-9.amzn2.0.6', + vulnerabilityCount: 1, + }, + ], + topVulnerabilities: [ + { + cve: 'CVE-2015-8390', + packageFixVersion: '2.56.1-9.amzn2.0.6', + packageName: 'glib2', + packageVersion: '2.56.1-9.amzn2.0.5', + severity: 'MEDIUM', + vulnerabilityCount: 1, + cvss: { + score: 9.800000190734863, + version: '3.1', + }, + }, + { + cve: 'CVE-2015-8394', + packageFixVersion: '2.56.1-9.amzn2.0.6', + packageName: 'glib2', + packageVersion: '2.56.1-9.amzn2.0.5', + severity: 'HIGH', + vulnerabilityCount: 1, + cvss: { + score: 9.800000190734863, + version: '3.1', + }, + }, + ], + }); + }); + + it('returns a 400 error when necessary indices are nonexistent', async () => { + await index.deleteFindingsIndex(); + + await supertest + .get('/internal/cloud_security_posture/vulnerabilities_dashboard') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .expect(500); + }); + }); +} diff --git a/x-pack/test/cloud_security_posture_functional/config.ts b/x-pack/test/cloud_security_posture_functional/config.ts index 09e2d2308c78d2..05f1477d84af17 100644 --- a/x-pack/test/cloud_security_posture_functional/config.ts +++ b/x-pack/test/cloud_security_posture_functional/config.ts @@ -38,7 +38,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { * 2. merge the updated version number change to kibana */ `--xpack.fleet.packages.0.name=cloud_security_posture`, - `--xpack.fleet.packages.0.version=1.2.10`, + `--xpack.fleet.packages.0.version=1.5.0`, // `--xpack.fleet.registryUrl=https://localhost:8080`, ], }, diff --git a/x-pack/test/cloud_security_posture_functional/mocks/vulnerabilities_latest_mock.ts b/x-pack/test/cloud_security_posture_functional/mocks/vulnerabilities_latest_mock.ts new file mode 100644 index 00000000000000..813c72743e7fc8 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/mocks/vulnerabilities_latest_mock.ts @@ -0,0 +1,259 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const vulnerabilitiesLatestMock = [ + { + agent: { + name: 'ip-172-31-17-52', + id: '9e69d889-c0c0-4c3e-9dc7-01dcd7c7db10', + ephemeral_id: '4fce6ed8-c9b3-40bb-b805-77e884cce0ef', + type: 'cloudbeat', + version: '8.8.0', + }, + package: { + path: 'snap-07b9e0d5eb5f324a1 (amazon 2 (Karoo))', + fixed_version: '2.56.1-9.amzn2.0.6', + name: 'glib2', + type: 'amazon', + version: '2.56.1-9.amzn2.0.5', + }, + resource: { + name: 'name-ng-1-Node', + id: '02d62a7df23951b19', + }, + elastic_agent: { + id: '9e69d889-c0c0-4c3e-9dc7-01dcd7c7db10', + version: '8.8.0', + snapshot: false, + }, + vulnerability: { + severity: 'MEDIUM', + package: { + fixed_version: '2.56.1-9.amzn2.0.6', + name: 'glib2', + version: '2.56.1-9.amzn2.0.5', + }, + description: + 'PCRE before 8.38 mishandles the [: and \\\\ substrings in character classes, which allows remote attackers to cause a denial of service (uninitialized memory read) or possibly have unspecified other impact via a crafted regular expression, as demonstrated by a JavaScript RegExp object encountered by Konqueror.', + title: + 'pcre: uninitialized memory read triggered by malformed posix character class (8.38/22)', + classification: 'CVSS', + data_source: { + ID: 'amazon', + URL: 'https://alas.aws.amazon.com/', + Name: 'Amazon Linux Security Center', + }, + cwe: ['CWE-908'], + reference: 'https://avd.aquasec.com/nvd/cve-2015-8390', + score: { + version: '3.1', + base: 9.8, + }, + report_id: 1687955586, + scanner: { + vendor: 'Trivy', + version: 'v0.35.0', + }, + id: 'CVE-2015-8390', + enumeration: 'CVE', + cvss: { + redhat: { + V2Vector: 'AV:N/AC:M/Au:N/C:N/I:N/A:P', + V2Score: 4.3, + }, + nvd: { + V3Vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H', + V2Vector: 'AV:N/AC:L/Au:N/C:P/I:P/A:P', + V3Score: 9.8, + V2Score: 7.5, + }, + }, + class: 'os-pkgs', + published_date: '2015-12-02T01:59:00Z', + }, + cloud: { + provider: 'aws', + region: 'eu-west-1', + account: { + name: 'elastic-security-cloud-security-dev', + id: '704479110758', + }, + }, + '@timestamp': '2023-06-29T02:08:44.993Z', + cloudbeat: { + commit_sha: '4d990caa0c9c1594441da6bf24a685599aeb2bd5', + commit_time: '2023-05-15T14:48:10Z', + version: '8.8.0', + }, + ecs: { + version: '8.6.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'cloud_security_posture.vulnerabilities', + }, + host: { + name: 'ip-172-31-17-52', + }, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 1687955586, + ingested: '2023-07-13T09:55:39Z', + kind: 'state', + created: '2023-06-29T02:08:44.993386561Z', + id: '80d05bca-9900-4038-ac8d-bcefaf6afd0c', + type: ['info'], + category: ['vulnerability'], + dataset: 'cloud_security_posture.vulnerabilities', + outcome: 'success', + }, + }, + { + agent: { + name: 'ip-172-31-17-52', + id: '9e69d889-c0c0-4c3e-9dc7-01dcd7c7db11', + type: 'cloudbeat', + ephemeral_id: '4fce6ed8-c9b3-40bb-b805-77e884cce0ef', + version: '8.8.0', + }, + package: { + path: 'snap-08c227d5c8a3dc1f2 (amazon 2 (Karoo))', + fixed_version: '2.56.1-9.amzn2.0.6', + name: 'glib2', + type: 'amazon', + version: '2.56.1-9.amzn2.0.5', + }, + resource: { + name: 'othername-june12-8-8-0-1', + id: '09d11277683ea41c5', + }, + elastic_agent: { + id: '9e69d889-c0c0-4c3e-9dc7-01dcd7c7db11', + version: '8.8.0', + snapshot: false, + }, + vulnerability: { + severity: 'HIGH', + package: { + fixed_version: '2.56.1-9.amzn2.0.6', + name: 'glib2', + version: '2.56.1-9.amzn2.0.5', + }, + description: + 'PCRE before 8.38 mishandles the (?() and (?(R) conditions, which allows remote attackers to cause a denial of service (integer overflow) or possibly have unspecified other impact via a crafted regular expression, as demonstrated by a JavaScript RegExp object encountered by Konqueror.', + classification: 'CVSS', + title: 'pcre: Integer overflow caused by missing check for certain conditions (8.38/31)', + data_source: { + ID: 'amazon', + URL: 'https://alas.aws.amazon.com/', + Name: 'Amazon Linux Security Center', + }, + reference: 'https://avd.aquasec.com/nvd/cve-2015-8394', + cwe: ['CWE-190'], + score: { + version: '3.1', + base: 9.8, + }, + report_id: 1687955586, + scanner: { + vendor: 'Trivy', + version: 'v0.35.0', + }, + id: 'CVE-2015-8394', + enumeration: 'CVE', + class: 'os-pkgs', + published_date: '2015-12-02T01:59:00Z', + cvss: { + redhat: { + V2Vector: 'AV:N/AC:M/Au:N/C:N/I:N/A:P', + V2Score: 4.3, + }, + nvd: { + V3Vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H', + V2Vector: 'AV:N/AC:L/Au:N/C:P/I:P/A:P', + V3Score: 9.8, + V2Score: 7.5, + }, + }, + }, + cloud: { + provider: 'aws', + region: 'eu-west-1', + account: { + name: 'elastic-security-cloud-security-dev', + id: '704479110758', + }, + }, + '@timestamp': '2023-06-29T02:08:16.535Z', + ecs: { + version: '8.6.0', + }, + cloudbeat: { + commit_sha: '4d990caa0c9c1594441da6bf24a685599aeb2bd5', + commit_time: '2023-05-15T14:48:10Z', + version: '8.8.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'cloud_security_posture.vulnerabilities', + }, + host: { + name: 'ip-172-31-17-52', + }, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 1687955586, + ingested: '2023-07-13T09:55:39Z', + created: '2023-06-29T02:08:16.535506246Z', + kind: 'state', + id: '7cd4309a-01dd-433c-8c44-14019f8b1522', + category: ['vulnerability'], + type: ['info'], + dataset: 'cloud_security_posture.vulnerabilities', + outcome: 'success', + }, + }, +]; + +export const scoresVulnerabilitiesMock = [ + { + critical: 0, + high: 1, + medium: 1, + low: 0, + policy_template: 'vuln_mgmt', + vulnerabilities_stats_by_cloud_account: { + '704479110758': { + cloudAccountName: 'elastic-security-cloud-security-dev', + cloudAccountId: '704479110758', + critical: 0, + high: 1, + medium: 1, + low: 0, + }, + }, + }, + { + critical: 0, + high: 1, + medium: 1, + low: 0, + policy_template: 'vuln_mgmt', + vulnerabilities_stats_by_cloud_account: { + '704479110758': { + cloudAccountName: 'elastic-security-cloud-security-dev', + cloudAccountId: '704479110758', + critical: 0, + high: 1, + medium: 1, + low: 0, + }, + }, + }, +]; diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts index 5774ca9eabc81a..56b83c6cc2d7ff 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts @@ -113,11 +113,6 @@ export function CspDashboardPageProvider({ getService, getPageObjects }: FtrProv await dashboard.getKubernetesSummarySection(); return await testSubjects.find('dashboard-summary-section-compliance-score'); }, - - getKubernetesComplianceScore2: async () => { - // await dashboard.getKubernetesSummarySection(); - return await testSubjects.find('dashboard-summary-section-compliance-score'); - }, }; const navigateToComplianceDashboardPage = async () => { diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts index ab0e0302e163d8..bd3a43951bf25d 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts @@ -273,6 +273,10 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider const notInstalledVulnerabilities = createNotInstalledObject('cnvm-integration-not-installed'); const notInstalledCSP = createNotInstalledObject('cloud_posture_page_package_not_installed'); + const vulnerabilityDataGrid = { + getVulnerabilityTable: async () => testSubjects.find('euiDataGrid'), + }; + const createFlyoutObject = (tableTestSubject: string) => ({ async getElement() { return await testSubjects.find(tableTestSubject); @@ -320,6 +324,7 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider index, waitForPluginInitialized, distributionBar, + vulnerabilityDataGrid, misconfigurationsFlyout, toastMessage, detectionRuleApi, diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/index.ts b/x-pack/test/cloud_security_posture_functional/page_objects/index.ts index 26aacd8cca997e..71f5619498c069 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/index.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/index.ts @@ -8,9 +8,11 @@ import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; import { FindingsPageProvider } from './findings_page'; import { CspDashboardPageProvider } from './csp_dashboard_page'; +import { VulnerabilityDashboardPageProvider } from './vulnerability_dashboard_page_object'; export const pageObjects = { ...xpackFunctionalPageObjects, findings: FindingsPageProvider, cloudPostureDashboard: CspDashboardPageProvider, + vulnerabilityDashboard: VulnerabilityDashboardPageProvider, }; diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/vulnerability_dashboard_page_object.ts b/x-pack/test/cloud_security_posture_functional/page_objects/vulnerability_dashboard_page_object.ts new file mode 100644 index 00000000000000..688786972debdf --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/page_objects/vulnerability_dashboard_page_object.ts @@ -0,0 +1,120 @@ +/* + * 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 { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +const VULNERABILITIES_LATEST_INDEX = 'logs-cloud_security_posture.vulnerabilities_latest-default'; +const BENCHMARK_SCORES_INDEX = 'logs-cloud_security_posture.scores-default'; + +export function VulnerabilityDashboardPageProvider({ + getService, + getPageObjects, +}: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'header']); + const retry = getService('retry'); + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + const testSubjects = getService('testSubjects'); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + const navigateToVulnerabilityDashboardPage = async () => { + await PageObjects.common.navigateToUrl( + 'securitySolution', // Defined in Security Solution plugin + 'cloud_security_posture/vulnerability_dashboard', + { shouldUseHashForSubUrl: false } + ); + }; + + const index = { + addFindings: async (vulnerabilitiesMock: T[]) => { + await Promise.all( + vulnerabilitiesMock.map((vulnerabilityDoc) => + es.index({ + index: VULNERABILITIES_LATEST_INDEX, + body: vulnerabilityDoc, + refresh: true, + }) + ) + ); + }, + + addScores: async (scoresMock: T[]) => { + await Promise.all( + scoresMock.map((scoreDoc) => + es.index({ + index: BENCHMARK_SCORES_INDEX, + body: scoreDoc, + refresh: true, + }) + ) + ); + }, + + removeFindings: async () => { + const indexExists = await es.indices.exists({ index: VULNERABILITIES_LATEST_INDEX }); + + if (indexExists) { + await es.deleteByQuery({ + index: VULNERABILITIES_LATEST_INDEX, + query: { match_all: {} }, + refresh: true, + }); + } + }, + + removeScores: async () => { + const indexExists = await es.indices.exists({ index: BENCHMARK_SCORES_INDEX }); + + if (indexExists) { + await es.deleteByQuery({ + index: BENCHMARK_SCORES_INDEX, + query: { match_all: {} }, + refresh: true, + }); + } + }, + + deleteFindingsIndex: async () => { + const indexExists = await es.indices.exists({ index: VULNERABILITIES_LATEST_INDEX }); + + if (indexExists) { + await es.indices.delete({ index: VULNERABILITIES_LATEST_INDEX }); + } + }, + }; + + const dashboard = { + getDashboardPageHeader: () => testSubjects.find('vulnerability-dashboard-page-header'), + + getCriticalStat: () => testSubjects.find('critical-count-stat'), + getHighStat: () => testSubjects.find('high-count-stat'), + getMediumStat: () => testSubjects.find('medium-count-stat'), + }; + + return { + navigateToVulnerabilityDashboardPage, + waitForPluginInitialized, + index, + dashboard, + }; +} diff --git a/x-pack/test/cloud_security_posture_functional/pages/index.ts b/x-pack/test/cloud_security_posture_functional/pages/index.ts index 81e905ddaca35a..c1bcdaea38cf8e 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/index.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/index.ts @@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./findings')); loadTestFile(require.resolve('./findings_alerts')); loadTestFile(require.resolve('./compliance_dashboard')); + loadTestFile(require.resolve('./vulnerability_dashboard')); }); } diff --git a/x-pack/test/cloud_security_posture_functional/pages/vulnerability_dashboard.ts b/x-pack/test/cloud_security_posture_functional/pages/vulnerability_dashboard.ts new file mode 100644 index 00000000000000..7fc9cd1cc946c7 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/pages/vulnerability_dashboard.ts @@ -0,0 +1,95 @@ +/* + * 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 type { FtrProviderContext } from '../ftr_provider_context'; +import { vulnerabilitiesLatestMock } from '../mocks/vulnerabilities_latest_mock'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const retry = getService('retry'); + const filterBar = getService('filterBar'); + const pageObjects = getPageObjects(['common', 'vulnerabilityDashboard', 'findings']); + + describe('Vulnerability Dashboard Page', function () { + this.tags(['cloud_security_vulnerability_dashboard']); + + let navigateToVulnerabilityDashboardPage: typeof pageObjects.vulnerabilityDashboard.navigateToVulnerabilityDashboardPage; + let waitForPluginInitialized: typeof pageObjects.vulnerabilityDashboard.waitForPluginInitialized; + let index: typeof pageObjects.vulnerabilityDashboard.index; + let dashboard: typeof pageObjects.vulnerabilityDashboard.dashboard; + + before(async () => { + navigateToVulnerabilityDashboardPage = + pageObjects.vulnerabilityDashboard.navigateToVulnerabilityDashboardPage; + waitForPluginInitialized = pageObjects.vulnerabilityDashboard.waitForPluginInitialized; + index = pageObjects.vulnerabilityDashboard.index; + dashboard = pageObjects.vulnerabilityDashboard.dashboard; + + await waitForPluginInitialized(); + + await index.addFindings(vulnerabilitiesLatestMock); + await navigateToVulnerabilityDashboardPage(); + await retry.waitFor( + 'Vulnerability dashboard to be displayed', + async () => !!dashboard.getDashboardPageHeader() + ); + }); + + after(async () => { + await index.removeFindings(); + }); + + describe('Vulnerability Dashboard', () => { + it('Page Header renders on startup', async () => { + const vulnPageHeader = await dashboard.getDashboardPageHeader(); + + expect( + (await vulnPageHeader.getVisibleText()) === 'Cloud Native Vulnerability Management' + ).to.be(true); + }); + + it('Stats render accurate output', async () => { + const criticalStat = await dashboard.getCriticalStat(); + const highStat = await dashboard.getHighStat(); + const mediumStat = await dashboard.getMediumStat(); + + const criticalCount = await criticalStat.findByTagName('span'); + const highCount = await highStat.findByTagName('span'); + const mediumCount = await mediumStat.findByTagName('span'); + + expect((await criticalCount.getVisibleText()) === '0').to.be(true); + expect((await highCount.getVisibleText()) === '1').to.be(true); + expect((await mediumCount.getVisibleText()) === '1').to.be(true); + }); + + it('should navigate to vulnerability findings page with high severity filter', async () => { + const stat = await dashboard.getHighStat(); + await stat.click(); + + const isFilterApplied = await filterBar.hasFilter('vulnerability.severity', 'HIGH'); + expect(isFilterApplied).to.be(true); + + // not removing the filter on purpose, to make sure it doesn't exist when navigating again from dashboard + await navigateToVulnerabilityDashboardPage(); + }); + + it('should navigate to vulnerability findings page with critical severity filter and no high severity filter', async () => { + const stat = await dashboard.getCriticalStat(); + await stat.click(); + + const isHighFilterApplied = await filterBar.hasFilter('vulnerability.severity', 'HIGH'); + expect(isHighFilterApplied).to.be(false); + const isFilterApplied = await filterBar.hasFilter('vulnerability.severity', 'CRITICAL'); + expect(isFilterApplied).to.be(true); + + await filterBar.removeFilter('vulnerability.severity'); + await navigateToVulnerabilityDashboardPage(); + }); + }); + }); +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index de73481e8473eb..acdfe3875a0df5 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -144,6 +144,8 @@ "@kbn/coloring", "@kbn/profiling-utils", "@kbn/profiling-data-access-plugin", + "@kbn/ecs", + "@kbn/coloring", "@kbn/es", ] } From 105935ace5d97272ce5d5565419f6d2d74d490d8 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 3 Oct 2023 12:15:34 +0300 Subject: [PATCH 09/24] [TSVB] Stabilize flaky TSVB test (#167848) ## Summary Closes https://github.com/elastic/kibana/issues/167791 https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3282 Flaky runner (50 times) ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- test/functional/page_objects/visual_builder_page.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 4c5939f728f015..6f106d143627a7 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -663,6 +663,7 @@ export class VisualBuilderPageObject extends FtrService { * @memberof VisualBuilderPage */ public async setFieldForAggregation(field: string, aggNth: number = 0): Promise { + await this.visChart.waitForVisualizationRenderingStabilized(); const fieldEl = await this.getFieldForAggregation(aggNth); await this.comboBox.setElement(fieldEl, field); From 0c6dfbf2094f1debfe3b78c90a24da92a6edb70d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 3 Oct 2023 11:19:49 +0200 Subject: [PATCH 10/24] [ML] ELSER v2 download in the Trained Models UI (#167407) ## Summary Adds support for ELSER v2 download from the Trained Models UI. - Marks an appropriate model version for the current cluster configuration with the recommended flag. - Updates the state column with better human-readable labels and colour indicators. - Adds a callout promoting a new version of ELSER image #### Notes for reviews - We need to wait for https://github.com/elastic/elasticsearch/pull/99584 to get the start deployment validation functionality. At the moment you can successfully start deployment of the wrong model version. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl= - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + .../packages/ml/trained_models_utils/index.ts | 5 + .../src/constants/trained_models.ts | 11 +- x-pack/plugins/ml/common/types/storage.ts | 5 + .../get_model_state_color.tsx | 61 ++++++ .../model_management/model_actions.tsx | 29 ++- .../model_management/models_list.tsx | 207 +++++++++++++----- .../model_management/model_provider.test.ts | 83 +++++++ .../model_management/models_provider.ts | 35 ++- .../services/ml/trained_models_table.ts | 11 +- 10 files changed, 373 insertions(+), 75 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/model_management/get_model_state_color.tsx diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 6beb073f2ae89d..ca81f5554ded86 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -510,6 +510,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { setUpgradeMode: `${ELASTICSEARCH_DOCS}ml-set-upgrade-mode.html`, trainedModels: `${MACHINE_LEARNING_DOCS}ml-trained-models.html`, startTrainedModelsDeployment: `${MACHINE_LEARNING_DOCS}ml-nlp-deploy-model.html`, + nlpElser: `${MACHINE_LEARNING_DOCS}ml-nlp-elser.html`, }, transforms: { guide: `${ELASTICSEARCH_DOCS}transforms.html`, diff --git a/x-pack/packages/ml/trained_models_utils/index.ts b/x-pack/packages/ml/trained_models_utils/index.ts index 22b808bdc7b5ee..0ae43c5ef40138 100644 --- a/x-pack/packages/ml/trained_models_utils/index.ts +++ b/x-pack/packages/ml/trained_models_utils/index.ts @@ -20,4 +20,9 @@ export { type ModelDefinitionResponse, type ElserVersion, type GetElserOptions, + ELSER_ID_V1, + ELASTIC_MODEL_TAG, + ELASTIC_MODEL_TYPE, + MODEL_STATE, + type ModelState, } from './src/constants/trained_models'; diff --git a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts index 4580330119ddd9..7bf4bbafad90d1 100644 --- a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts +++ b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts @@ -46,8 +46,12 @@ export const BUILT_IN_MODEL_TAG = 'prepackaged'; export const ELASTIC_MODEL_TAG = 'elastic'; +export const ELSER_ID_V1 = '.elser_model_1' as const; + export const ELASTIC_MODEL_DEFINITIONS: Record = Object.freeze({ '.elser_model_1': { + modelName: 'elser', + hidden: true, version: 1, config: { input: { @@ -59,6 +63,7 @@ export const ELASTIC_MODEL_DEFINITIONS: Record = Object }), }, '.elser_model_2_SNAPSHOT': { + modelName: 'elser', version: 2, default: true, config: { @@ -71,6 +76,7 @@ export const ELASTIC_MODEL_DEFINITIONS: Record = Object }), }, '.elser_model_2_linux-x86_64_SNAPSHOT': { + modelName: 'elser', version: 2, os: 'Linux', arch: 'amd64', @@ -87,6 +93,7 @@ export const ELASTIC_MODEL_DEFINITIONS: Record = Object } as const); export interface ModelDefinition { + modelName: string; version: number; config: object; description: string; @@ -94,9 +101,10 @@ export interface ModelDefinition { arch?: string; default?: boolean; recommended?: boolean; + hidden?: boolean; } -export type ModelDefinitionResponse = ModelDefinition & { +export type ModelDefinitionResponse = Omit & { name: string; }; @@ -106,6 +114,7 @@ export const MODEL_STATE = { ...DEPLOYMENT_STATE, DOWNLOADING: 'downloading', DOWNLOADED: 'downloaded', + NOT_DOWNLOADED: 'notDownloaded', } as const; export type ModelState = typeof MODEL_STATE[keyof typeof MODEL_STATE] | null; diff --git a/x-pack/plugins/ml/common/types/storage.ts b/x-pack/plugins/ml/common/types/storage.ts index a74bbea0e3affa..7213fa134c1a56 100644 --- a/x-pack/plugins/ml/common/types/storage.ts +++ b/x-pack/plugins/ml/common/types/storage.ts @@ -15,6 +15,7 @@ export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference'; export const ML_ANOMALY_EXPLORER_PANELS = 'ml.anomalyExplorerPanels'; export const ML_NOTIFICATIONS_LAST_CHECKED_AT = 'ml.notificationsLastCheckedAt'; export const ML_OVERVIEW_PANELS = 'ml.overviewPanels'; +export const ML_ELSER_CALLOUT_DISMISSED = 'ml.elserUpdateCalloutDismissed'; export type PartitionFieldConfig = | { @@ -68,6 +69,7 @@ export interface MlStorageRecord { [ML_ANOMALY_EXPLORER_PANELS]: AnomalyExplorerPanelsState | undefined; [ML_NOTIFICATIONS_LAST_CHECKED_AT]: number | undefined; [ML_OVERVIEW_PANELS]: OverviewPanelsState; + [ML_ELSER_CALLOUT_DISMISSED]: boolean | undefined; } export type MlStorage = Partial | null; @@ -88,6 +90,8 @@ export type TMlStorageMapped = T extends typeof ML_ENTIT ? number | undefined : T extends typeof ML_OVERVIEW_PANELS ? OverviewPanelsState | undefined + : T extends typeof ML_ELSER_CALLOUT_DISMISSED + ? boolean | undefined : null; export const ML_STORAGE_KEYS = [ @@ -98,4 +102,5 @@ export const ML_STORAGE_KEYS = [ ML_ANOMALY_EXPLORER_PANELS, ML_NOTIFICATIONS_LAST_CHECKED_AT, ML_OVERVIEW_PANELS, + ML_ELSER_CALLOUT_DISMISSED, ] as const; diff --git a/x-pack/plugins/ml/public/application/model_management/get_model_state_color.tsx b/x-pack/plugins/ml/public/application/model_management/get_model_state_color.tsx new file mode 100644 index 00000000000000..4bc7caa390c39a --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/get_model_state_color.tsx @@ -0,0 +1,61 @@ +/* + * 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 { MODEL_STATE, ModelState } from '@kbn/ml-trained-models-utils'; +import { EuiHealthProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const getModelStateColor = ( + state: ModelState +): { color: EuiHealthProps['color']; name: string } | null => { + switch (state) { + case MODEL_STATE.DOWNLOADED: + return { + color: 'subdued', + name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.downloadedName', { + defaultMessage: 'Ready to deploy', + }), + }; + case MODEL_STATE.DOWNLOADING: + return { + color: 'warning', + name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.downloadingName', { + defaultMessage: 'Downloading...', + }), + }; + case MODEL_STATE.STARTED: + return { + color: 'success', + name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.startedName', { + defaultMessage: 'Deployed', + }), + }; + case MODEL_STATE.STARTING: + return { + color: 'success', + name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.startingName', { + defaultMessage: 'Starting deployment...', + }), + }; + case MODEL_STATE.STOPPING: + return { + color: 'accent', + name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.stoppingName', { + defaultMessage: 'Stopping deployment...', + }), + }; + case MODEL_STATE.NOT_DOWNLOADED: + return { + color: '#d4dae5', + name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.notDownloadedName', { + defaultMessage: 'Not downloaded', + }), + }; + default: + return null; + } +}; diff --git a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx index f10ba782ee9da7..7c08717b407236 100644 --- a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx +++ b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx @@ -177,17 +177,18 @@ export function useModelActions({ }, { name: i18n.translate('xpack.ml.inference.modelsList.startModelDeploymentActionLabel', { - defaultMessage: 'Start deployment', + defaultMessage: 'Deploy', }), description: i18n.translate( - 'xpack.ml.inference.modelsList.startModelDeploymentActionLabel', + 'xpack.ml.inference.modelsList.startModelDeploymentActionDescription', { defaultMessage: 'Start deployment', } ), 'data-test-subj': 'mlModelsTableRowStartDeploymentAction', + // @ts-ignore EUI has a type check issue when type "button" is combined with an icon. icon: 'play', - type: 'icon', + type: 'button', isPrimary: true, enabled: (item) => { return canStartStopTrainedModels && !isLoading && item.state !== MODEL_STATE.DOWNLOADING; @@ -311,10 +312,12 @@ export function useModelActions({ 'data-test-subj': 'mlModelsTableRowStopDeploymentAction', icon: 'stop', type: 'icon', - isPrimary: true, - available: (item) => item.model_type === TRAINED_MODEL_TYPE.PYTORCH, - enabled: (item) => - canStartStopTrainedModels && !isLoading && item.deployment_ids.length > 0, + isPrimary: false, + available: (item) => + item.model_type === TRAINED_MODEL_TYPE.PYTORCH && + canStartStopTrainedModels && + (item.state === MODEL_STATE.STARTED || item.state === MODEL_STATE.STARTING), + enabled: (item) => !isLoading, onClick: async (item) => { const requireForceStop = isPopulatedObject(item.pipelines); const hasMultipleDeployments = item.deployment_ids.length > 1; @@ -380,17 +383,19 @@ export function useModelActions({ }, { name: i18n.translate('xpack.ml.inference.modelsList.downloadModelActionLabel', { - defaultMessage: 'Download model', + defaultMessage: 'Download', }), description: i18n.translate('xpack.ml.inference.modelsList.downloadModelActionLabel', { - defaultMessage: 'Download model', + defaultMessage: 'Download', }), 'data-test-subj': 'mlModelsTableRowDownloadModelAction', + // @ts-ignore EUI has a type check issue when type "button" is combined with an icon. icon: 'download', - type: 'icon', + type: 'button', isPrimary: true, - available: (item) => item.tags.includes(ELASTIC_MODEL_TAG), - enabled: (item) => !item.state && !isLoading, + available: (item) => + item.tags.includes(ELASTIC_MODEL_TAG) && item.state === MODEL_STATE.NOT_DOWNLOADED, + enabled: (item) => !isLoading, onClick: async (item) => { try { onLoading(true); diff --git a/x-pack/plugins/ml/public/application/model_management/models_list.tsx b/x-pack/plugins/ml/public/application/model_management/models_list.tsx index b959c1e2340647..adb36221d99853 100644 --- a/x-pack/plugins/ml/public/application/model_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/model_management/models_list.tsx @@ -10,12 +10,16 @@ import { EuiBadge, EuiButton, EuiButtonIcon, + EuiCallOut, EuiFlexGroup, EuiFlexItem, + EuiHealth, EuiInMemoryTable, - EuiSearchBarProps, + EuiLink, + type EuiSearchBarProps, EuiSpacer, EuiTitle, + EuiToolTip, SearchFilterConfig, } from '@elastic/eui'; import { groupBy } from 'lodash'; @@ -28,18 +32,21 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { usePageUrlState } from '@kbn/ml-url-state'; import { useTimefilter } from '@kbn/ml-date-picker'; import { - BUILT_IN_MODEL_TYPE, BUILT_IN_MODEL_TAG, + BUILT_IN_MODEL_TYPE, DEPLOYMENT_STATE, -} from '@kbn/ml-trained-models-utils'; -import { isDefined } from '@kbn/ml-is-defined'; -import { ELASTIC_MODEL_DEFINITIONS, ELASTIC_MODEL_TAG, ELASTIC_MODEL_TYPE, + ELSER_ID_V1, MODEL_STATE, - ModelState, -} from '@kbn/ml-trained-models-utils/src/constants/trained_models'; + type ModelState, +} from '@kbn/ml-trained-models-utils'; +import { isDefined } from '@kbn/ml-is-defined'; +import { css } from '@emotion/react'; +import { useStorage } from '@kbn/ml-local-storage'; +import { getModelStateColor } from './get_model_state_color'; +import { ML_ELSER_CALLOUT_DISMISSED } from '../../../common/types/storage'; import { TechnicalPreviewBadge } from '../components/technical_preview_badge'; import { useModelActions } from './model_actions'; import { ModelsTableToConfigMapping } from '.'; @@ -74,6 +81,7 @@ export type ModelItem = TrainedModelConfigResponse & { deployment_ids: string[]; putModelConfig?: object; state: ModelState; + recommended?: boolean; }; export type ModelItemFull = Required; @@ -83,10 +91,14 @@ interface PageUrlState { pageUrlState: ListingPageUrlState; } +const modelIdColumnName = i18n.translate('xpack.ml.trainedModels.modelsList.modelIdHeader', { + defaultMessage: 'ID', +}); + export const getDefaultModelsListState = (): ListingPageUrlState => ({ pageIndex: 0, pageSize: 10, - sortField: ModelsTableToConfigMapping.id, + sortField: modelIdColumnName, sortDirection: 'asc', }); @@ -102,10 +114,17 @@ export const ModelsList: FC = ({ const { services: { application: { capabilities }, + docLinks, }, } = useMlKibana(); + const nlpElserDocUrl = docLinks.links.ml.nlpElser; + const { isNLPEnabled } = useEnabledFeatures(); + const [isElserCalloutDismissed, setIsElserCalloutDismissed] = useStorage( + ML_ELSER_CALLOUT_DISMISSED, + false + ); useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true }); @@ -155,6 +174,11 @@ export const ModelsList: FC = ({ [] ); + // List of downloaded/existing models + const existingModels = useMemo(() => { + return items.filter((i) => !i.putModelConfig); + }, [items]); + /** * Checks if the model download complete. */ @@ -219,7 +243,35 @@ export const ModelsList: FC = ({ // TODO combine fetching models definitions and stats into a single function await fetchModelsStats(newItems); - setItems(newItems); + let resultItems = newItems; + // don't add any of the built-in models (e.g. elser) if NLP is disabled + if (isNLPEnabled) { + const idMap = new Map( + resultItems.map((model) => [model.model_id, model]) + ); + const forDownload = await trainedModelsApiService.getTrainedModelDownloads(); + const notDownloaded: ModelItem[] = forDownload + .filter(({ name, hidden, recommended }) => { + if (recommended && idMap.has(name)) { + idMap.get(name)!.recommended = true; + } + return !idMap.has(name) && !hidden; + }) + .map((modelDefinition) => { + return { + model_id: modelDefinition.name, + type: [ELASTIC_MODEL_TYPE], + tags: [ELASTIC_MODEL_TAG], + putModelConfig: modelDefinition.config, + description: modelDefinition.description, + state: MODEL_STATE.NOT_DOWNLOADED, + recommended: !!modelDefinition.recommended, + } as ModelItem; + }); + resultItems = [...resultItems, ...notDownloaded]; + } + + setItems(resultItems); if (expandedItemsToRefresh.length > 0) { await fetchModelsStats(expandedItemsToRefresh); @@ -242,7 +294,7 @@ export const ModelsList: FC = ({ setIsInitialized(true); setIsLoading(false); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [itemIdToExpandedRowMap]); + }, [itemIdToExpandedRowMap, isNLPEnabled]); useEffect( function updateOnTimerRefresh() { @@ -257,13 +309,13 @@ export const ModelsList: FC = ({ return { total: { show: true, - value: items.length, + value: existingModels.length, label: i18n.translate('xpack.ml.trainedModels.modelsList.totalAmountLabel', { defaultMessage: 'Total trained models', }), }, }; - }, [items]); + }, [existingModels]); /** * Fetches models stats and update the original object @@ -325,7 +377,7 @@ export const ModelsList: FC = ({ * Unique inference types from models */ const inferenceTypesOptions = useMemo(() => { - const result = items.reduce((acc, item) => { + const result = existingModels.reduce((acc, item) => { const type = item.inference_config && Object.keys(item.inference_config)[0]; if (type) { acc.add(type); @@ -339,13 +391,16 @@ export const ModelsList: FC = ({ value: v, name: v, })); - }, [items]); + }, [existingModels]); const modelAndDeploymentIds = useMemo( () => [ - ...new Set([...items.flatMap((v) => v.deployment_ids), ...items.map((i) => i.model_id)]), + ...new Set([ + ...existingModels.flatMap((v) => v.deployment_ids), + ...existingModels.map((i) => i.model_id), + ]), ], - [items] + [existingModels] ); /** @@ -400,30 +455,61 @@ export const ModelsList: FC = ({ 'data-test-subj': 'mlModelsTableRowDetailsToggle', }, { - field: ModelsTableToConfigMapping.id, - name: i18n.translate('xpack.ml.trainedModels.modelsList.modelIdHeader', { - defaultMessage: 'ID', - }), - sortable: true, + name: modelIdColumnName, + width: '15%', + sortable: ({ model_id: modelId }: ModelItem) => modelId, truncateText: false, + textOnly: false, 'data-test-subj': 'mlModelsTableColumnId', + render: ({ description, model_id: modelId }: ModelItem) => { + const isTechPreview = description?.includes('(Tech Preview)'); + + return ( + + {modelId} + {isTechPreview ? ( + + + + ) : null} + + ); + }, }, { - field: ModelsTableToConfigMapping.description, - width: '350px', + width: '35%', name: i18n.translate('xpack.ml.trainedModels.modelsList.modelDescriptionHeader', { defaultMessage: 'Description', }), - sortable: false, truncateText: false, 'data-test-subj': 'mlModelsTableColumnDescription', - render: (description: string) => { + render: ({ description, recommended }: ModelItem) => { if (!description) return null; - const isTechPreview = description.includes('(Tech Preview)'); return ( <> {description.replace('(Tech Preview)', '')} - {isTechPreview ? : null} + {recommended ? ( + + } + > + +   + + + + ) : null} ); }, @@ -456,8 +542,13 @@ export const ModelsList: FC = ({ }), align: 'left', truncateText: false, - render: (state: string) => { - return state ? {state} : null; + render: (state: ModelState) => { + const config = getModelStateColor(state); + return config ? ( + + {config.name} + + ) : null; }, 'data-test-subj': 'mlModelsTableColumnDeploymentState', }, @@ -585,28 +676,8 @@ export const ModelsList: FC = ({ : {}), }; - const resultItems = useMemo(() => { - if (isNLPEnabled === false) { - // don't add any of the built in models (e.g. elser) if NLP is disabled - return items; - } - - const idSet = new Set(items.map((i) => i.model_id)); - const notDownloaded: ModelItem[] = Object.entries(ELASTIC_MODEL_DEFINITIONS) - .filter(([modelId]) => !idSet.has(modelId)) - .map(([modelId, modelDefinition]) => { - return { - model_id: modelId, - type: [ELASTIC_MODEL_TYPE], - tags: [ELASTIC_MODEL_TAG], - putModelConfig: modelDefinition.config, - description: modelDefinition.description, - } as ModelItem; - }); - const result = [...items, ...notDownloaded]; - - return result; - }, [isNLPEnabled, items]); + const isElserCalloutVisible = + !isElserCalloutDismissed && items.findIndex((i) => i.model_id === ELSER_ID_V1) >= 0; if (!isInitialized) return null; @@ -631,7 +702,7 @@ export const ModelsList: FC = ({ isExpandable={true} itemIdToExpandedRowMap={itemIdToExpandedRowMap} isSelectable={false} - items={resultItems} + items={items} itemId={ModelsTableToConfigMapping.id} loading={isLoading} search={search} @@ -643,6 +714,38 @@ export const ModelsList: FC = ({ onTableChange={onTableChange} sorting={sorting} data-test-subj={isLoading ? 'mlModelsTable loading' : 'mlModelsTable loaded'} + childrenBetween={ + isElserCalloutVisible ? ( + <> + + } + onDismiss={setIsElserCalloutDismissed.bind(null, true)} + > + + + + ), + }} + /> + + + + ) : null + } /> {modelsToDelete.length > 0 && ( diff --git a/x-pack/plugins/ml/server/models/model_management/model_provider.test.ts b/x-pack/plugins/ml/server/models/model_management/model_provider.test.ts index 7e66d03033b66e..052e5d1f536446 100644 --- a/x-pack/plugins/ml/server/models/model_management/model_provider.test.ts +++ b/x-pack/plugins/ml/server/models/model_management/model_provider.test.ts @@ -43,6 +43,89 @@ describe('modelsProvider', () => { jest.clearAllMocks(); }); + describe('getModelDownloads', () => { + test('provides a list of models with recommended and default flag', async () => { + const result = await modelService.getModelDownloads(); + expect(result).toEqual([ + { + config: { input: { field_names: ['text_field'] } }, + description: 'Elastic Learned Sparse EncodeR v1 (Tech Preview)', + hidden: true, + name: '.elser_model_1', + version: 1, + }, + { + config: { input: { field_names: ['text_field'] } }, + default: true, + description: 'Elastic Learned Sparse EncodeR v2 (Tech Preview)', + name: '.elser_model_2_SNAPSHOT', + version: 2, + }, + { + arch: 'amd64', + config: { input: { field_names: ['text_field'] } }, + description: + 'Elastic Learned Sparse EncodeR v2, optimized for linux-x86_64 (Tech Preview)', + name: '.elser_model_2_linux-x86_64_SNAPSHOT', + os: 'Linux', + recommended: true, + version: 2, + }, + ]); + }); + + test('provides a list of models with default model as recommended', async () => { + mockCloud.cloudId = undefined; + (mockClient.asInternalUser.transport.request as jest.Mock).mockResolvedValueOnce({ + _nodes: { + total: 1, + successful: 1, + failed: 0, + }, + cluster_name: 'default', + nodes: { + yYmqBqjpQG2rXsmMSPb9pQ: { + name: 'node-0', + roles: ['ml'], + attributes: {}, + os: { + name: 'Mac OS X', + arch: 'aarch64', + }, + }, + }, + }); + + const result = await modelService.getModelDownloads(); + + expect(result).toEqual([ + { + config: { input: { field_names: ['text_field'] } }, + description: 'Elastic Learned Sparse EncodeR v1 (Tech Preview)', + hidden: true, + name: '.elser_model_1', + version: 1, + }, + { + config: { input: { field_names: ['text_field'] } }, + recommended: true, + description: 'Elastic Learned Sparse EncodeR v2 (Tech Preview)', + name: '.elser_model_2_SNAPSHOT', + version: 2, + }, + { + arch: 'amd64', + config: { input: { field_names: ['text_field'] } }, + description: + 'Elastic Learned Sparse EncodeR v2, optimized for linux-x86_64 (Tech Preview)', + name: '.elser_model_2_linux-x86_64_SNAPSHOT', + os: 'Linux', + version: 2, + }, + ]); + }); + }); + describe('getELSER', () => { test('provides a recommended definition by default', async () => { const result = await modelService.getELSER(); diff --git a/x-pack/plugins/ml/server/models/model_management/models_provider.ts b/x-pack/plugins/ml/server/models/model_management/models_provider.ts index d05096bef81893..c10cf19076de46 100644 --- a/x-pack/plugins/ml/server/models/model_management/models_provider.ts +++ b/x-pack/plugins/ml/server/models/model_management/models_provider.ts @@ -25,6 +25,7 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { PipelineDefinition } from '../../../common/types/trained_models'; export type ModelService = ReturnType; + export const modelsProvider = (client: IScopedClusterClient, cloud?: CloudSetup) => new ModelsProvider(client, cloud); @@ -83,6 +84,7 @@ export class ModelsProvider { return { [index]: null }; } } + private getNodeId( elementOriginalId: string, nodeType: typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES] @@ -446,18 +448,39 @@ export class ModelsProvider { } } - const result = Object.entries(ELASTIC_MODEL_DEFINITIONS).map(([name, def]) => { + const modelDefinitionMap = new Map(); + + for (const [name, def] of Object.entries(ELASTIC_MODEL_DEFINITIONS)) { const recommended = (isCloud && def.os === 'Linux' && def.arch === 'amd64') || (sameArch && !!def?.os && def?.os === osName && def?.arch === arch); - return { - ...def, - name, + + const { modelName, ...rest } = def; + + const modelDefinitionResponse = { + ...rest, ...(recommended ? { recommended } : {}), + name, }; - }); - return result; + if (modelDefinitionMap.has(modelName)) { + modelDefinitionMap.get(modelName)!.push(modelDefinitionResponse); + } else { + modelDefinitionMap.set(modelName, [modelDefinitionResponse]); + } + } + + // check if there is no recommended, so we mark default as recommended + for (const arr of modelDefinitionMap.values()) { + const defaultModel = arr.find((a) => a.default); + const recommendedModel = arr.find((a) => a.recommended); + if (defaultModel && !recommendedModel) { + delete defaultModel.default; + defaultModel.recommended = true; + } + } + + return [...modelDefinitionMap.values()].flat(); } /** diff --git a/x-pack/test/functional/services/ml/trained_models_table.ts b/x-pack/test/functional/services/ml/trained_models_table.ts index 52f5beab5c2484..6616d61a78f429 100644 --- a/x-pack/test/functional/services/ml/trained_models_table.ts +++ b/x-pack/test/functional/services/ml/trained_models_table.ts @@ -264,9 +264,11 @@ export function TrainedModelsTableProvider( } public async deleteModel(modelId: string) { + const fromContextMenu = await this.doesModelCollapsedActionsButtonExist(modelId); await mlCommonUI.invokeTableRowAction( this.rowSelector(modelId), - 'mlModelsTableRowDeleteAction' + 'mlModelsTableRowDeleteAction', + fromContextMenu ); await this.assertDeleteModalExists(); await this.confirmDeleteModel(); @@ -459,9 +461,10 @@ export function TrainedModelsTableProvider( } public async clickStopDeploymentAction(modelId: string) { - await testSubjects.clickWhenNotDisabled( - this.rowSelector(modelId, 'mlModelsTableRowStopDeploymentAction'), - { timeout: 5000 } + await mlCommonUI.invokeTableRowAction( + this.rowSelector(modelId), + 'mlModelsTableRowStopDeploymentAction', + true ); } From 17f633c420150c3a304042e2dea42abaa22a72a8 Mon Sep 17 00:00:00 2001 From: Katerina Date: Tue, 3 Oct 2023 11:55:39 +0200 Subject: [PATCH 11/24] [APM] Introduce custom dashboards tab in service overview (#166789) ## Summary https://github.com/elastic/kibana/assets/3369346/3598bfb7-83fc-4eb0-b185-20d689442a68 ### Notes 1. ~~Storing the dashboard name in the saved object may become outdated and cause confusion, as users have the ability to update the dashboard title on the dashboard page. [**UPDATED**] Fetch dynamically from the dashboard module api 3. UI we don't have an indicator useContextFilter ## TODO - [x] API tests - [x] Dynamic title - [x] Deep-link for dashboard - [x] Fetch services that match the dashboard kuery --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../current_mappings.json | 16 + packages/kbn-shared-svg/index.ts | 4 +- .../src/assets/dashboards_dark.svg | 1 + .../src/assets/dashboards_light.svg | 1 + .../group2/check_registered_types.test.ts | 1 + .../group3/type_registrations.test.ts | 1 + .../group5/dot_kibana_split.test.ts | 1 + .../plugins/apm/common/custom_dashboards.ts | 20 ++ .../app/metrics/static_dashboard/index.tsx | 4 +- .../actions/edit_dashboard.tsx | 44 +++ .../actions/goto_dashboard.tsx | 41 +++ .../app/service_dashboards/actions/index.ts | 12 + .../actions/link_dashboard.tsx | 58 ++++ .../actions/save_dashboard_modal.tsx | 276 ++++++++++++++++++ .../actions/unlink_dashboard.tsx | 113 +++++++ .../app/service_dashboards/context_menu.tsx | 56 ++++ .../service_dashboards/dashboard_selector.tsx | 89 ++++++ .../service_dashboards/empty_dashboards.tsx | 79 +++++ .../app/service_dashboards/index.tsx | 225 ++++++++++++++ .../routing/service_detail/index.tsx | 15 + .../templates/apm_service_template/index.tsx | 14 +- .../public/hooks/use_dashboards_fetcher.ts | 56 ++++ x-pack/plugins/apm/public/plugin.ts | 3 +- x-pack/plugins/apm/server/plugin.ts | 2 + .../get_global_apm_server_route_repository.ts | 2 + .../get_custom_dashboards.ts | 37 +++ .../get_services_with_dashboards.ts | 86 ++++++ .../remove_service_dashboard.ts | 23 ++ .../server/routes/custom_dashboards/route.ts | 114 ++++++++ .../save_service_dashboard.ts | 44 +++ .../saved_objects/apm_custom_dashboards.ts | 46 +++ .../plugins/apm/server/saved_objects/index.ts | 1 + x-pack/plugins/apm/tsconfig.json | 1 + .../tests/custom_dashboards/api_helper.ts | 75 +++++ .../custom_dashboards.spec.ts | 194 ++++++++++++ 35 files changed, 1750 insertions(+), 5 deletions(-) create mode 100644 packages/kbn-shared-svg/src/assets/dashboards_dark.svg create mode 100644 packages/kbn-shared-svg/src/assets/dashboards_light.svg create mode 100644 x-pack/plugins/apm/common/custom_dashboards.ts create mode 100644 x-pack/plugins/apm/public/components/app/service_dashboards/actions/edit_dashboard.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_dashboards/actions/goto_dashboard.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_dashboards/actions/index.ts create mode 100644 x-pack/plugins/apm/public/components/app/service_dashboards/actions/link_dashboard.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_dashboards/context_menu.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_dashboards/empty_dashboards.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_dashboards/index.tsx create mode 100644 x-pack/plugins/apm/public/hooks/use_dashboards_fetcher.ts create mode 100644 x-pack/plugins/apm/server/routes/custom_dashboards/get_custom_dashboards.ts create mode 100644 x-pack/plugins/apm/server/routes/custom_dashboards/get_services_with_dashboards.ts create mode 100644 x-pack/plugins/apm/server/routes/custom_dashboards/remove_service_dashboard.ts create mode 100644 x-pack/plugins/apm/server/routes/custom_dashboards/route.ts create mode 100644 x-pack/plugins/apm/server/routes/custom_dashboards/save_service_dashboard.ts create mode 100644 x-pack/plugins/apm/server/saved_objects/apm_custom_dashboards.ts create mode 100644 x-pack/test/apm_api_integration/tests/custom_dashboards/api_helper.ts create mode 100644 x-pack/test/apm_api_integration/tests/custom_dashboards/custom_dashboards.spec.ts diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 2f8ad9cf0a18bd..2d77538b095726 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -3131,6 +3131,22 @@ } } }, + "apm-custom-dashboards": { + "properties": { + "dashboardSavedObjectId": { + "type": "keyword" + }, + "kuery": { + "type": "text" + }, + "serviceEnvironmentFilterEnabled": { + "type": "boolean" + }, + "serviceNameFilterEnabled": { + "type": "boolean" + } + } + }, "enterprise_search_telemetry": { "dynamic": false, "properties": {} diff --git a/packages/kbn-shared-svg/index.ts b/packages/kbn-shared-svg/index.ts index 61ffc0f763cbaa..215431706ab94e 100644 --- a/packages/kbn-shared-svg/index.ts +++ b/packages/kbn-shared-svg/index.ts @@ -8,5 +8,7 @@ import noResultsIllustrationDark from './src/assets/no_results_dark.svg'; import noResultsIllustrationLight from './src/assets/no_results_light.svg'; +import dashboardsLight from './src/assets/dashboards_light.svg'; +import dashboardsDark from './src/assets/dashboards_dark.svg'; -export { noResultsIllustrationDark, noResultsIllustrationLight }; +export { noResultsIllustrationDark, noResultsIllustrationLight, dashboardsLight, dashboardsDark }; diff --git a/packages/kbn-shared-svg/src/assets/dashboards_dark.svg b/packages/kbn-shared-svg/src/assets/dashboards_dark.svg new file mode 100644 index 00000000000000..6499131ce6b5a9 --- /dev/null +++ b/packages/kbn-shared-svg/src/assets/dashboards_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/kbn-shared-svg/src/assets/dashboards_light.svg b/packages/kbn-shared-svg/src/assets/dashboards_light.svg new file mode 100644 index 00000000000000..4ca82bfe3ff98e --- /dev/null +++ b/packages/kbn-shared-svg/src/assets/dashboards_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts index 423008ee1eecea..b5a1591e7a5bd2 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts @@ -59,6 +59,7 @@ describe('checking migration metadata changes on all registered SO types', () => "action_task_params": "96e27e7f4e8273ffcd87060221e2b75e81912dd5", "alert": "dc710bc17dfc98a9a703d388569abccce5f8bf07", "api_key_pending_invalidation": "1399e87ca37b3d3a65d269c924eda70726cfe886", + "apm-custom-dashboards": "b67128f78160c288bd7efe25b2da6e2afd5e82fc", "apm-indices": "8a2d68d415a4b542b26b0d292034a28ffac6fed4", "apm-server-schema": "58a8c6468edae3d1dc520f0134f59cf3f4fd7eff", "apm-service-group": "66dfc1ddd40bad8f693c873bf6002ca30079a4ae", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index efb439e058cc2d..2cef3801868bd3 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -15,6 +15,7 @@ const previouslyRegisteredTypes = [ 'action_task_params', 'alert', 'api_key_pending_invalidation', + 'apm-custom-dashboards', 'apm-indices', 'apm-server-schema', 'apm-service-group', diff --git a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts index 3c41eafb6102c4..c39ceaf30da692 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts @@ -181,6 +181,7 @@ describe('split .kibana index into multiple system indices', () => { "action_task_params", "alert", "api_key_pending_invalidation", + "apm-custom-dashboards", "apm-indices", "apm-server-schema", "apm-service-group", diff --git a/x-pack/plugins/apm/common/custom_dashboards.ts b/x-pack/plugins/apm/common/custom_dashboards.ts new file mode 100644 index 00000000000000..7e289d970b2e6e --- /dev/null +++ b/x-pack/plugins/apm/common/custom_dashboards.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const APM_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE = 'apm-custom-dashboards'; + +export interface ApmCustomDashboard { + dashboardSavedObjectId: string; + serviceNameFilterEnabled: boolean; + serviceEnvironmentFilterEnabled: boolean; + kuery?: string; +} + +export interface SavedApmCustomDashboard extends ApmCustomDashboard { + id: string; + updatedAt: number; +} diff --git a/x-pack/plugins/apm/public/components/app/metrics/static_dashboard/index.tsx b/x-pack/plugins/apm/public/components/app/metrics/static_dashboard/index.tsx index 789239ffa1d682..50bda742eb377b 100644 --- a/x-pack/plugins/apm/public/components/app/metrics/static_dashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/metrics/static_dashboard/index.tsx @@ -109,7 +109,7 @@ async function getCreationOptions( } } -function getFilters( +export function getFilters( serviceName: string, environment: string, dataView: DataView @@ -139,7 +139,7 @@ function getFilters( } else { const environmentFilter = buildPhraseFilter( environmentField, - serviceName, + environment, dataView ); filters.push(environmentFilter); diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/edit_dashboard.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/edit_dashboard.tsx new file mode 100644 index 00000000000000..e3a6619b446d61 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/edit_dashboard.tsx @@ -0,0 +1,44 @@ +/* + * 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 { EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { SaveDashboardModal } from './save_dashboard_modal'; +import { MergedServiceDashboard } from '..'; + +export function EditDashboard({ + onRefresh, + currentDashboard, +}: { + onRefresh: () => void; + currentDashboard: MergedServiceDashboard; +}) { + const [isModalVisible, setIsModalVisible] = useState(false); + return ( + <> + setIsModalVisible(!isModalVisible)} + > + {i18n.translate('xpack.apm.serviceDashboards.editEmptyButtonLabel', { + defaultMessage: 'Edit dashboard link', + })} + + + {isModalVisible && ( + setIsModalVisible(!isModalVisible)} + onRefresh={onRefresh} + currentDashboard={currentDashboard} + /> + )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/goto_dashboard.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/goto_dashboard.tsx new file mode 100644 index 00000000000000..f196077c41a4d1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/goto_dashboard.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiButtonEmpty } from '@elastic/eui'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ApmPluginStartDeps } from '../../../../plugin'; +import { SavedApmCustomDashboard } from '../../../../../common/custom_dashboards'; + +export function GotoDashboard({ + currentDashboard, +}: { + currentDashboard: SavedApmCustomDashboard; +}) { + const { + services: { + dashboard: { locator: dashboardLocator }, + }, + } = useKibana(); + + const url = dashboardLocator?.getRedirectUrl({ + dashboardId: currentDashboard?.dashboardSavedObjectId, + }); + return ( + + {i18n.translate('xpack.apm.serviceDashboards.contextMenu.goToDashboard', { + defaultMessage: 'Go to dashboard', + })} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/index.ts b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/index.ts new file mode 100644 index 00000000000000..c37616318239f5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LinkDashboard } from './link_dashboard'; +import { GotoDashboard } from './goto_dashboard'; +import { EditDashboard } from './edit_dashboard'; + +export { LinkDashboard, GotoDashboard, EditDashboard }; diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/link_dashboard.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/link_dashboard.tsx new file mode 100644 index 00000000000000..7b652c21039d86 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/link_dashboard.tsx @@ -0,0 +1,58 @@ +/* + * 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 { EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { MergedServiceDashboard } from '..'; +import { SaveDashboardModal } from './save_dashboard_modal'; + +export function LinkDashboard({ + onRefresh, + emptyButton = false, + serviceDashboards, +}: { + onRefresh: () => void; + emptyButton?: boolean; + serviceDashboards?: MergedServiceDashboard[]; +}) { + const [isModalVisible, setIsModalVisible] = useState(false); + + return ( + <> + {emptyButton ? ( + setIsModalVisible(true)} + > + {i18n.translate('xpack.apm.serviceDashboards.linkEmptyButtonLabel', { + defaultMessage: 'Link new dashboard', + })} + + ) : ( + setIsModalVisible(true)} + > + {i18n.translate('xpack.apm.serviceDashboards.linkButtonLabel', { + defaultMessage: 'Link dashboard', + })} + + )} + + {isModalVisible && ( + setIsModalVisible(false)} + onRefresh={onRefresh} + serviceDashboards={serviceDashboards} + /> + )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx new file mode 100644 index 00000000000000..81dc0ba157a017 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useState } from 'react'; +import { + EuiButton, + EuiModal, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSwitch, + EuiModalBody, + EuiComboBox, + EuiComboBoxOptionOption, + EuiFlexGroup, + EuiToolTip, + EuiIcon, + EuiButtonEmpty, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DashboardItem } from '@kbn/dashboard-plugin/common/content_management'; +import { callApmApi } from '../../../../services/rest/create_call_apm_api'; +import { useDashboardFetcher } from '../../../../hooks/use_dashboards_fetcher'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useApmParams } from '../../../../hooks/use_apm_params'; +import { SERVICE_NAME } from '../../../../../common/es_fields/apm'; +import { MergedServiceDashboard } from '..'; + +interface Props { + onClose: () => void; + onRefresh: () => void; + currentDashboard?: MergedServiceDashboard; + serviceDashboards?: MergedServiceDashboard[]; +} + +export function SaveDashboardModal({ + onClose, + onRefresh, + currentDashboard, + serviceDashboards, +}: Props) { + const { + core: { notifications }, + } = useApmPluginContext(); + const { data: allAvailableDashboards, status } = useDashboardFetcher(); + + let defaultOption: EuiComboBoxOptionOption | undefined; + + const [serviceFiltersEnabled, setserviceFiltersEnabled] = useState( + (currentDashboard?.serviceEnvironmentFilterEnabled && + currentDashboard?.serviceNameFilterEnabled) ?? + true + ); + + if (currentDashboard) { + const { title, dashboardSavedObjectId } = currentDashboard; + defaultOption = { label: title, value: dashboardSavedObjectId }; + } + + const [selectedDashboard, setSelectedDashboard] = useState( + defaultOption ? [defaultOption] : [] + ); + + const isEditMode = !!currentDashboard?.id; + + const { + path: { serviceName }, + } = useApmParams('/services/{serviceName}/dashboards'); + + const reloadCustomDashboards = useCallback(() => { + onRefresh(); + }, [onRefresh]); + + const options = allAvailableDashboards?.map( + (dashboardItem: DashboardItem) => ({ + label: dashboardItem.attributes.title, + value: dashboardItem.id, + disabled: + serviceDashboards?.some( + ({ dashboardSavedObjectId }) => + dashboardItem.id === dashboardSavedObjectId + ) ?? false, + }) + ); + const onSave = useCallback( + async function () { + const [newDashboard] = selectedDashboard; + try { + if (newDashboard.value) { + await callApmApi('POST /internal/apm/custom-dashboard', { + params: { + query: { customDashboardId: currentDashboard?.id }, + body: { + dashboardSavedObjectId: newDashboard.value, + serviceEnvironmentFilterEnabled: serviceFiltersEnabled, + serviceNameFilterEnabled: serviceFiltersEnabled, + kuery: `${SERVICE_NAME}: ${serviceName}`, + }, + }, + signal: null, + }); + + notifications.toasts.addSuccess( + isEditMode + ? getEditSuccessToastLabels(newDashboard.label) + : getLinkSuccessToastLabels(newDashboard.label) + ); + reloadCustomDashboards(); + } + } catch (error) { + console.error(error); + notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.serviceDashboards.addFailure.toast.title', + { + defaultMessage: 'Error while adding "{dashboardName}" dashboard', + values: { dashboardName: newDashboard.label }, + } + ), + text: error.body.message, + }); + } + onClose(); + }, + [ + selectedDashboard, + notifications.toasts, + serviceFiltersEnabled, + onClose, + reloadCustomDashboards, + isEditMode, + serviceName, + currentDashboard, + ] + ); + + return ( + + + + {isEditMode + ? i18n.translate( + 'xpack.apm.serviceDashboards.selectDashboard.modalTitle.edit', + { + defaultMessage: 'Edit dashboard', + } + ) + : i18n.translate( + 'xpack.apm.serviceDashboards.selectDashboard.modalTitle.link', + { + defaultMessage: 'Select dashboard', + } + )} + + + + + + setSelectedDashboard(newSelection)} + isClearable={true} + /> + + + {i18n.translate( + 'xpack.apm.dashboard.addDashboard.useContextFilterLabel', + { + defaultMessage: 'Filter by service and environment', + } + )}{' '} + + + +

      + } + onChange={() => setserviceFiltersEnabled(!serviceFiltersEnabled)} + checked={serviceFiltersEnabled} + /> +
      +
      + + + + {i18n.translate( + 'xpack.apm.serviceDashboards.selectDashboard.cancel', + { + defaultMessage: 'Cancel', + } + )} + + + {isEditMode + ? i18n.translate( + 'xpack.apm.serviceDashboards.selectDashboard.edit', + { + defaultMessage: 'Save', + } + ) + : i18n.translate( + 'xpack.apm.serviceDashboards.selectDashboard.add', + { + defaultMessage: 'Link dashboard', + } + )} + + +
      + ); +} + +function getLinkSuccessToastLabels(dashboardName: string) { + return { + title: i18n.translate( + 'xpack.apm.serviceDashboards.linkSuccess.toast.title', + { + defaultMessage: 'Added "{dashboardName}" dashboard', + values: { dashboardName }, + } + ), + text: i18n.translate('xpack.apm.serviceDashboards.linkSuccess.toast.text', { + defaultMessage: + 'Your dashboard is now visible in the service overview page.', + }), + }; +} + +function getEditSuccessToastLabels(dashboardName: string) { + return { + title: i18n.translate( + 'xpack.apm.serviceDashboards.editSuccess.toast.title', + { + defaultMessage: 'Edited "{dashboardName}" dashboard', + values: { dashboardName }, + } + ), + text: i18n.translate('xpack.apm.serviceDashboards.editSuccess.toast.text', { + defaultMessage: 'Your dashboard link have been updated', + }), + }; +} diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx new file mode 100644 index 00000000000000..b0dbda84bb6cf8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx @@ -0,0 +1,113 @@ +/* + * 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 { EuiButtonEmpty, EuiConfirmModal } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useState } from 'react'; +import { MergedServiceDashboard } from '..'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { callApmApi } from '../../../../services/rest/create_call_apm_api'; + +export function UnlinkDashboard({ + currentDashboard, + onRefresh, +}: { + currentDashboard: MergedServiceDashboard; + onRefresh: () => void; +}) { + const [isModalVisible, setIsModalVisible] = useState(false); + const { + core: { notifications }, + } = useApmPluginContext(); + + const onConfirm = useCallback( + async function () { + try { + await callApmApi('DELETE /internal/apm/custom-dashboard', { + params: { query: { customDashboardId: currentDashboard.id } }, + signal: null, + }); + + notifications.toasts.addSuccess({ + title: i18n.translate( + 'xpack.apm.serviceDashboards.unlinkSuccess.toast.title', + { + defaultMessage: 'Unlinked "{dashboardName}" dashboard', + values: { dashboardName: currentDashboard?.title }, + } + ), + }); + onRefresh(); + } catch (error) { + console.error(error); + notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.serviceDashboards.unlinkFailure.toast.title', + { + defaultMessage: + 'Error while unlinking "{dashboardName}" dashboard', + values: { dashboardName: currentDashboard?.title }, + } + ), + text: error.body.message, + }); + } + setIsModalVisible(!isModalVisible); + }, + [ + currentDashboard, + notifications.toasts, + setIsModalVisible, + onRefresh, + isModalVisible, + ] + ); + return ( + <> + setIsModalVisible(true)} + > + {i18n.translate('xpack.apm.serviceDashboards.unlinkEmptyButtonLabel', { + defaultMessage: 'Unlink dashboard', + })} + + {isModalVisible && ( + setIsModalVisible(false)} + onConfirm={onConfirm} + confirmButtonText={i18n.translate( + 'xpack.apm.serviceDashboards.unlinkEmptyButtonLabel.confirm.button', + { + defaultMessage: 'Unlink dashboard', + } + )} + buttonColor="danger" + defaultFocusedButton="confirm" + > +

      + {i18n.translate( + 'xpack.apm.serviceDashboards.unlinkEmptyButtonLabel.confirm.body', + { + defaultMessage: + 'You are about to unlink the dashboard from the service context', + } + )} +

      +
      + )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/context_menu.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/context_menu.tsx new file mode 100644 index 00000000000000..2eb48b7f668484 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/context_menu.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { + EuiButtonIcon, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiPopover, +} from '@elastic/eui'; + +interface Props { + items: React.ReactNode[]; +} + +export function ContextMenu({ items }: Props) { + const [isPopoverOpen, setPopover] = useState(false); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + return ( + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + ( + {item} + ))} + /> + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx new file mode 100644 index 00000000000000..115b97ad41cc86 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { EuiComboBox } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { MergedServiceDashboard } from '.'; +import { fromQuery, toQuery } from '../../shared/links/url_helpers'; + +interface Props { + serviceDashboards: MergedServiceDashboard[]; + currentDashboard?: MergedServiceDashboard; + handleOnChange: (selectedId?: string) => void; +} + +export function DashboardSelector({ + serviceDashboards, + currentDashboard, + handleOnChange, +}: Props) { + const history = useHistory(); + + useEffect( + () => + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(location.search), + dashboardId: currentDashboard?.id, + }), + }), + // It should only update when loaded + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + function onChange(newDashboardId?: string) { + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(location.search), + dashboardId: newDashboardId, + }), + }); + handleOnChange(newDashboardId); + } + return ( + { + return { + label: title, + value: dashboardSavedObjectId, + }; + })} + selectedOptions={ + currentDashboard + ? [ + { + value: currentDashboard?.dashboardSavedObjectId, + label: currentDashboard?.title, + }, + ] + : [] + } + onChange={([newItem]) => onChange(newItem.value)} + isClearable={false} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/empty_dashboards.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/empty_dashboards.tsx new file mode 100644 index 00000000000000..843a2c47b26492 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/empty_dashboards.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiEmptyPrompt, EuiImage } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { dashboardsDark, dashboardsLight } from '@kbn/shared-svg'; +import { useTheme } from '../../../hooks/use_theme'; + +interface Props { + actions: React.ReactNode; +} + +export function EmptyDashboards({ actions }: Props) { + const theme = useTheme(); + + return ( + <> + + } + title={ +

      + {i18n.translate('xpack.apm.serviceDashboards.emptyTitle', { + defaultMessage: + 'The best way to understand your data is to visualize it.', + })} +

      + } + layout="horizontal" + color="plain" + body={ + <> +
        +
      • + {i18n.translate('xpack.apm.serviceDashboards.emptyBody.first', { + defaultMessage: 'bring clarity to your data', + })} +
      • +
      • + {i18n.translate( + 'xpack.apm.serviceDashboards.emptyBody.second', + { + defaultMessage: 'tell a story about your data', + } + )} +
      • +
      • + {i18n.translate('xpack.apm.serviceDashboards.emptyBody', { + defaultMessage: + 'focus on only the data that’s important to you', + })} +
      • +
      +

      + {i18n.translate( + 'xpack.apm.serviceDashboards.emptyBody.getStarted', + { + defaultMessage: 'To get started, add your dashaboard', + } + )} +

      + + } + actions={actions} + /> + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/index.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/index.tsx new file mode 100644 index 00000000000000..f5df58b95cc410 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/index.tsx @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, + EuiEmptyPrompt, + EuiLoadingLogo, +} from '@elastic/eui'; + +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { + AwaitingDashboardAPI, + DashboardCreationOptions, + DashboardRenderer, +} from '@kbn/dashboard-plugin/public'; +import { EmptyDashboards } from './empty_dashboards'; +import { GotoDashboard, LinkDashboard } from './actions'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { SavedApmCustomDashboard } from '../../../../common/custom_dashboards'; +import { ContextMenu } from './context_menu'; +import { UnlinkDashboard } from './actions/unlink_dashboard'; +import { EditDashboard } from './actions/edit_dashboard'; +import { DashboardSelector } from './dashboard_selector'; +import { useApmDataView } from '../../../hooks/use_apm_data_view'; +import { getFilters } from '../metrics/static_dashboard'; +import { useDashboardFetcher } from '../../../hooks/use_dashboards_fetcher'; +import { useTimeRange } from '../../../hooks/use_time_range'; + +export interface MergedServiceDashboard extends SavedApmCustomDashboard { + title: string; +} + +export function ServiceDashboards() { + const { + path: { serviceName }, + query: { environment, kuery, rangeFrom, rangeTo, dashboardId }, + } = useApmParams('/services/{serviceName}/dashboards'); + const [dashboard, setDashboard] = useState(); + const [serviceDashboards, setServiceDashboards] = useState< + MergedServiceDashboard[] + >([]); + const [currentDashboard, setCurrentDashboard] = + useState(); + const { data: allAvailableDashboards } = useDashboardFetcher(); + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + + const { dataView } = useApmDataView(); + + const { data, status, refetch } = useFetcher( + (callApmApi) => { + if (serviceName) { + return callApmApi( + `GET /internal/apm/services/{serviceName}/dashboards`, + { + isCachable: false, + params: { + path: { serviceName }, + query: { start, end }, + }, + } + ); + } + }, + [serviceName, start, end] + ); + + useEffect(() => { + const filteredServiceDashbords = (data?.serviceDashboards ?? []).reduce( + ( + result: MergedServiceDashboard[], + serviceDashboard: SavedApmCustomDashboard + ) => { + const matchedDashboard = allAvailableDashboards.find( + ({ id }) => id === serviceDashboard.dashboardSavedObjectId + ); + if (matchedDashboard) { + result.push({ + title: matchedDashboard.attributes.title, + ...serviceDashboard, + }); + } + return result; + }, + [] + ); + + setServiceDashboards(filteredServiceDashbords); + + const preselectedDashboard = + filteredServiceDashbords.find( + ({ dashboardSavedObjectId }) => dashboardSavedObjectId === dashboardId + ) ?? filteredServiceDashbords[0]; + + // preselect dashboard + setCurrentDashboard(preselectedDashboard); + }, [allAvailableDashboards, data?.serviceDashboards, dashboardId]); + + const getCreationOptions = + useCallback((): Promise => { + const getInitialInput = () => ({ + viewMode: ViewMode.VIEW, + timeRange: { from: rangeFrom, to: rangeTo }, + }); + return Promise.resolve({ getInitialInput }); + }, [rangeFrom, rangeTo]); + + useEffect(() => { + if (!dashboard) return; + + dashboard.updateInput({ + filters: + dataView && + currentDashboard?.serviceEnvironmentFilterEnabled && + currentDashboard?.serviceNameFilterEnabled + ? getFilters(serviceName, environment, dataView) + : [], + timeRange: { from: rangeFrom, to: rangeTo }, + query: { query: kuery, language: 'kuery' }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + dataView, + serviceName, + environment, + kuery, + dashboard, + rangeFrom, + rangeTo, + ]); + + const handleOnChange = (selectedId?: string) => { + setCurrentDashboard( + serviceDashboards?.find( + ({ dashboardSavedObjectId }) => dashboardSavedObjectId === selectedId + ) + ); + }; + + return ( + + {status === FETCH_STATUS.LOADING ? ( + } + title={ +

      + {i18n.translate( + 'xpack.apm.serviceDashboards.loadingServiceDashboards', + { + defaultMessage: 'Loading service dashboard', + } + )} +

      + } + /> + ) : status === FETCH_STATUS.SUCCESS && serviceDashboards?.length > 0 ? ( + <> + + + +

      {currentDashboard?.title}

      +
      +
      + + + + + + {currentDashboard && ( + + , + , + , + , + ]} + /> + + )} +
      + + + {currentDashboard && ( + + )} + + + ) : ( + } /> + )} +
      + ); +} diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index 5817b96e2b3602..56deaaa2e6d6e6 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -39,6 +39,7 @@ import { ApmServiceWrapper } from './apm_service_wrapper'; import { RedirectToDefaultServiceRouteView } from './redirect_to_default_service_route_view'; import { ProfilingOverview } from '../../app/profiling_overview'; import { SearchBar } from '../../shared/search_bar/search_bar'; +import { ServiceDashboards } from '../../app/service_dashboards'; function page({ title, @@ -376,6 +377,20 @@ export const serviceDetailRoute = { }, }), }, + '/services/{serviceName}/dashboards': { + ...page({ + tab: 'dashboards', + title: i18n.translate('xpack.apm.views.dashboard.title', { + defaultMessage: 'Dashboards', + }), + element: , + }), + params: t.partial({ + query: t.partial({ + dashboardId: t.string, + }), + }), + }, '/services/{serviceName}/': { element: , }, diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index 2e97a0e6156c5f..b5261eb55826f9 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -63,7 +63,8 @@ type Tab = NonNullable[0] & { | 'service-map' | 'logs' | 'alerts' - | 'profiling'; + | 'profiling' + | 'dashboards'; hidden?: boolean; }; @@ -417,6 +418,17 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { ), }, + { + key: 'dashboards', + href: router.link('/services/{serviceName}/dashboards', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.home.dashboardsTabLabel', { + defaultMessage: 'Dashboards', + }), + append: , + }, ]; return tabs diff --git a/x-pack/plugins/apm/public/hooks/use_dashboards_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_dashboards_fetcher.ts new file mode 100644 index 00000000000000..c463d07276a3ab --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_dashboards_fetcher.ts @@ -0,0 +1,56 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { SearchDashboardsResponse } from '@kbn/dashboard-plugin/public/services/dashboard_content_management/lib/find_dashboards'; +import { ApmPluginStartDeps } from '../plugin'; +import { FETCH_STATUS } from './use_fetcher'; + +export interface SearchDashboardsResult { + data: SearchDashboardsResponse['hits']; + status: FETCH_STATUS; +} + +export function useDashboardFetcher(query?: string): SearchDashboardsResult { + const { + services: { dashboard }, + } = useKibana(); + + const [result, setResult] = useState({ + data: [], + status: FETCH_STATUS.NOT_INITIATED, + }); + + useEffect(() => { + const getDashboards = async () => { + setResult({ + data: [], + status: FETCH_STATUS.LOADING, + }); + try { + const findDashboardsService = await dashboard?.findDashboardsService(); + const data = await findDashboardsService.search({ + search: query ?? '', + size: 1000, + }); + + setResult({ + data: data.hits, + status: FETCH_STATUS.SUCCESS, + }); + } catch { + setResult({ + data: [], + status: FETCH_STATUS.FAILURE, + }); + } + }; + getDashboards(); + }, [dashboard, query]); + return result; +} diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 800fb6bf123cd1..f9206b8aaa7822 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -69,6 +69,7 @@ import type { import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import { DashboardStart } from '@kbn/dashboard-plugin/public'; import { from } from 'rxjs'; import { map } from 'rxjs/operators'; import type { ConfigSchema } from '.'; @@ -84,7 +85,6 @@ import { featureCatalogueEntry } from './feature_catalogue_entry'; import { APMServiceDetailLocator } from './locator/service_detail_locator'; import { ITelemetryClient, TelemetryService } from './services/telemetry'; export type ApmPluginSetup = ReturnType; - export type ApmPluginStart = void; export interface ApmPluginSetupDeps { @@ -136,6 +136,7 @@ export interface ApmPluginStartDeps { uiActions: UiActionsStart; profiling?: ProfilingPluginStart; observabilityAIAssistant: ObservabilityAIAssistantPluginStart; + dashboard: DashboardStart; } const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', { diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 96e47f0edad19e..525b2c5e2cbc52 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -33,6 +33,7 @@ import { apmTelemetry, apmServerSettings, apmServiceGroups, + apmCustomDashboards, } from './saved_objects'; import { APMPluginSetup, @@ -77,6 +78,7 @@ export class APMPlugin core.savedObjects.registerType(apmTelemetry); core.savedObjects.registerType(apmServerSettings); core.savedObjects.registerType(apmServiceGroups); + core.savedObjects.registerType(apmCustomDashboards); const currentConfig = this.initContext.config.get(); this.currentConfig = currentConfig; diff --git a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts index 4186523029c99c..7c555366c9e68e 100644 --- a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts @@ -46,6 +46,7 @@ import { traceRouteRepository } from '../traces/route'; import { transactionRouteRepository } from '../transactions/route'; import { assistantRouteRepository } from '../assistant_functions/route'; import { profilingRouteRepository } from '../profiling/route'; +import { serviceDashboardsRouteRepository } from '../custom_dashboards/route'; function getTypedGlobalApmServerRouteRepository() { const repository = { @@ -85,6 +86,7 @@ function getTypedGlobalApmServerRouteRepository() { ...diagnosticsRepository, ...assistantRouteRepository, ...profilingRouteRepository, + ...serviceDashboardsRouteRepository, }; return repository; diff --git a/x-pack/plugins/apm/server/routes/custom_dashboards/get_custom_dashboards.ts b/x-pack/plugins/apm/server/routes/custom_dashboards/get_custom_dashboards.ts new file mode 100644 index 00000000000000..14a942cd26844c --- /dev/null +++ b/x-pack/plugins/apm/server/routes/custom_dashboards/get_custom_dashboards.ts @@ -0,0 +1,37 @@ +/* + * 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 { SavedObjectsClientContract } from '@kbn/core/server'; +import { + APM_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + SavedApmCustomDashboard, + ApmCustomDashboard, +} from '../../../common/custom_dashboards'; + +interface Props { + savedObjectsClient: SavedObjectsClientContract; +} + +export async function getCustomDashboards({ + savedObjectsClient, +}: Props): Promise { + const result = await savedObjectsClient.find({ + type: APM_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + page: 1, + perPage: 1000, + sortField: 'updated_at', + sortOrder: 'desc', + }); + + return result.saved_objects.map( + ({ id, attributes, updated_at: upatedAt }) => ({ + id, + updatedAt: upatedAt ? Date.parse(upatedAt) : 0, + ...attributes, + }) + ); +} diff --git a/x-pack/plugins/apm/server/routes/custom_dashboards/get_services_with_dashboards.ts b/x-pack/plugins/apm/server/routes/custom_dashboards/get_services_with_dashboards.ts new file mode 100644 index 00000000000000..23a77588eb6cd9 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/custom_dashboards/get_services_with_dashboards.ts @@ -0,0 +1,86 @@ +/* + * 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 { + kqlQuery, + rangeQuery, + termQuery, +} from '@kbn/observability-plugin/server'; +import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { estypes } from '@elastic/elasticsearch'; +import { SERVICE_NAME } from '../../../common/es_fields/apm'; +import { + APMEventClient, + APMEventESSearchRequest, +} from '../../lib/helpers/create_es_client/create_apm_event_client'; +import { SavedApmCustomDashboard } from '../../../common/custom_dashboards'; + +function getSearchRequest( + filters: estypes.QueryDslQueryContainer[] +): APMEventESSearchRequest { + return { + apm: { + events: [ProcessorEvent.metric, ProcessorEvent.transaction], + }, + body: { + track_total_hits: false, + terminate_after: 1, + size: 1, + query: { + bool: { + filter: filters, + }, + }, + }, + }; +} +export async function getServicesWithDashboards({ + apmEventClient, + allLinkedCustomDashboards, + serviceName, + start, + end, +}: { + apmEventClient: APMEventClient; + allLinkedCustomDashboards: SavedApmCustomDashboard[]; + serviceName: string; + start: number; + end: number; +}): Promise { + const allKueryPerDashboard = allLinkedCustomDashboards.map(({ kuery }) => ({ + kuery, + })); + const allSearches = allKueryPerDashboard.map((dashboard) => + getSearchRequest([ + ...kqlQuery(dashboard.kuery), + ...termQuery(SERVICE_NAME, serviceName), + ...rangeQuery(start, end), + ]) + ); + + const filteredDashboards = []; + + if (allSearches.length > 0) { + const allResponses = ( + await apmEventClient.msearch( + 'get_services_with_dashboards', + ...allSearches + ) + ).responses; + + for (let index = 0; index < allLinkedCustomDashboards.length; index++) { + const responsePerDashboard = allResponses[index]; + const dashboard = allLinkedCustomDashboards[index]; + + if (responsePerDashboard.hits.hits.length > 0) { + filteredDashboards.push(dashboard); + } + } + } + + return filteredDashboards; +} diff --git a/x-pack/plugins/apm/server/routes/custom_dashboards/remove_service_dashboard.ts b/x-pack/plugins/apm/server/routes/custom_dashboards/remove_service_dashboard.ts new file mode 100644 index 00000000000000..5a7a7b0d69e0ee --- /dev/null +++ b/x-pack/plugins/apm/server/routes/custom_dashboards/remove_service_dashboard.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 { SavedObjectsClientContract } from '@kbn/core/server'; +import { APM_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE } from '../../../common/custom_dashboards'; + +interface Options { + savedObjectsClient: SavedObjectsClientContract; + customDashboardId: string; +} +export async function deleteServiceDashboard({ + savedObjectsClient, + customDashboardId, +}: Options) { + return savedObjectsClient.delete( + APM_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + customDashboardId + ); +} diff --git a/x-pack/plugins/apm/server/routes/custom_dashboards/route.ts b/x-pack/plugins/apm/server/routes/custom_dashboards/route.ts new file mode 100644 index 00000000000000..256cd2fb3cba90 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/custom_dashboards/route.ts @@ -0,0 +1,114 @@ +/* + * 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 { createApmServerRoute } from '../apm_routes/create_apm_server_route'; +import { saveServiceDashbord } from './save_service_dashboard'; +import { SavedApmCustomDashboard } from '../../../common/custom_dashboards'; +import { deleteServiceDashboard } from './remove_service_dashboard'; +import { getCustomDashboards } from './get_custom_dashboards'; +import { getServicesWithDashboards } from './get_services_with_dashboards'; +import { getApmEventClient } from '../../lib/helpers/get_apm_event_client'; +import { rangeRt } from '../default_api_types'; + +const serviceDashboardSaveRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/custom-dashboard', + params: t.type({ + query: t.union([ + t.partial({ + customDashboardId: t.string, + }), + t.undefined, + ]), + body: t.type({ + dashboardSavedObjectId: t.string, + kuery: t.union([t.string, t.undefined]), + serviceNameFilterEnabled: t.boolean, + serviceEnvironmentFilterEnabled: t.boolean, + }), + }), + options: { tags: ['access:apm', 'access:apm_write'] }, + handler: async (resources): Promise => { + const { context, params } = resources; + const { customDashboardId } = params.query; + const { + savedObjects: { client: savedObjectsClient }, + } = await context.core; + + return saveServiceDashbord({ + savedObjectsClient, + customDashboardId, + serviceDashboard: params.body, + }); + }, +}); + +const serviceDashboardsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/services/{serviceName}/dashboards', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: rangeRt, + }), + options: { + tags: ['access:apm'], + }, + handler: async ( + resources + ): Promise<{ serviceDashboards: SavedApmCustomDashboard[] }> => { + const { context, params } = resources; + const { start, end } = params.query; + + const { serviceName } = params.path; + + const apmEventClient = await getApmEventClient(resources); + + const { + savedObjects: { client: savedObjectsClient }, + } = await context.core; + + const allLinkedCustomDashboards = await getCustomDashboards({ + savedObjectsClient, + }); + + const servicesWithDashboards = await getServicesWithDashboards({ + apmEventClient, + allLinkedCustomDashboards, + serviceName, + start, + end, + }); + + return { serviceDashboards: servicesWithDashboards }; + }, +}); + +const serviceDashboardDeleteRoute = createApmServerRoute({ + endpoint: 'DELETE /internal/apm/custom-dashboard', + params: t.type({ + query: t.type({ + customDashboardId: t.string, + }), + }), + options: { tags: ['access:apm', 'access:apm_write'] }, + handler: async (resources): Promise => { + const { context, params } = resources; + const { customDashboardId } = params.query; + const savedObjectsClient = (await context.core).savedObjects.client; + await deleteServiceDashboard({ + savedObjectsClient, + customDashboardId, + }); + }, +}); + +export const serviceDashboardsRouteRepository = { + ...serviceDashboardSaveRoute, + ...serviceDashboardDeleteRoute, + ...serviceDashboardsRoute, +}; diff --git a/x-pack/plugins/apm/server/routes/custom_dashboards/save_service_dashboard.ts b/x-pack/plugins/apm/server/routes/custom_dashboards/save_service_dashboard.ts new file mode 100644 index 00000000000000..5c43dda2a4da52 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/custom_dashboards/save_service_dashboard.ts @@ -0,0 +1,44 @@ +/* + * 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 { SavedObjectsClientContract } from '@kbn/core/server'; +import { + APM_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + SavedApmCustomDashboard, + ApmCustomDashboard, +} from '../../../common/custom_dashboards'; + +interface Options { + savedObjectsClient: SavedObjectsClientContract; + customDashboardId?: string; + serviceDashboard: ApmCustomDashboard; +} +export async function saveServiceDashbord({ + savedObjectsClient, + customDashboardId, + serviceDashboard, +}: Options): Promise { + const { + id, + attributes, + updated_at: updatedAt, + } = await (customDashboardId + ? savedObjectsClient.update( + APM_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + customDashboardId, + serviceDashboard + ) + : savedObjectsClient.create( + APM_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + serviceDashboard + )); + return { + id, + ...(attributes as ApmCustomDashboard), + updatedAt: updatedAt ? Date.parse(updatedAt) : 0, + }; +} diff --git a/x-pack/plugins/apm/server/saved_objects/apm_custom_dashboards.ts b/x-pack/plugins/apm/server/saved_objects/apm_custom_dashboards.ts new file mode 100644 index 00000000000000..8d4b20757f136c --- /dev/null +++ b/x-pack/plugins/apm/server/saved_objects/apm_custom_dashboards.ts @@ -0,0 +1,46 @@ +/* + * 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 { SavedObjectsType } from '@kbn/core/server'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { APM_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE } from '../../common/custom_dashboards'; + +export const apmCustomDashboards: SavedObjectsType = { + name: APM_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'multiple', + mappings: { + properties: { + dashboardSavedObjectId: { type: 'keyword' }, + kuery: { type: 'text' }, + serviceEnvironmentFilterEnabled: { type: 'boolean' }, + serviceNameFilterEnabled: { type: 'boolean' }, + }, + }, + management: { + importableAndExportable: true, + icon: 'apmApp', + getTitle: () => + i18n.translate('xpack.apm.apmServiceDashboards.title', { + defaultMessage: 'APM Service Custom Dashboards', + }), + }, + modelVersions: { + '1': { + changes: [], + schemas: { + create: schema.object({ + dashboardSavedObjectId: schema.string(), + kuery: schema.maybe(schema.string()), + serviceEnvironmentFilterEnabled: schema.boolean(), + serviceNameFilterEnabled: schema.boolean(), + }), + }, + }, + }, +}; diff --git a/x-pack/plugins/apm/server/saved_objects/index.ts b/x-pack/plugins/apm/server/saved_objects/index.ts index b39e032ad14bd0..effcedfc689325 100644 --- a/x-pack/plugins/apm/server/saved_objects/index.ts +++ b/x-pack/plugins/apm/server/saved_objects/index.ts @@ -8,3 +8,4 @@ export { apmTelemetry } from './apm_telemetry'; export { apmServerSettings } from './apm_server_settings'; export { apmServiceGroups } from './apm_service_groups'; +export { apmCustomDashboards } from './apm_custom_dashboards'; diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index 7a22ac6e4a4c2e..2c225c509fad6a 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -102,6 +102,7 @@ "@kbn/core-analytics-server", "@kbn/analytics-client", "@kbn/monaco", + "@kbn/shared-svg", "@kbn/deeplinks-observability" ], "exclude": ["target/**/*"] diff --git a/x-pack/test/apm_api_integration/tests/custom_dashboards/api_helper.ts b/x-pack/test/apm_api_integration/tests/custom_dashboards/api_helper.ts new file mode 100644 index 00000000000000..a0fb0e976d1093 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/custom_dashboards/api_helper.ts @@ -0,0 +1,75 @@ +/* + * 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 { ApmApiClient } from '../../common/config'; + +export async function getServiceDashboardApi( + apmApiClient: ApmApiClient, + serviceName: string, + start: string, + end: string +) { + return apmApiClient.writeUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/dashboards', + params: { + path: { serviceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }); +} + +export async function getLinkServiceDashboardApi({ + dashboardSavedObjectId, + apmApiClient, + customDashboardId, + kuery, + serviceFiltersEnabled, +}: { + apmApiClient: ApmApiClient; + dashboardSavedObjectId: string; + customDashboardId?: string; + kuery: string; + serviceFiltersEnabled: boolean; +}) { + const response = await apmApiClient.writeUser({ + endpoint: 'POST /internal/apm/custom-dashboard', + params: { + query: { + customDashboardId, + }, + body: { + dashboardSavedObjectId, + kuery, + serviceEnvironmentFilterEnabled: serviceFiltersEnabled, + serviceNameFilterEnabled: serviceFiltersEnabled, + }, + }, + }); + return response; +} + +export async function deleteAllServiceDashboard( + apmApiClient: ApmApiClient, + serviceName: string, + start: string, + end: string +) { + return await getServiceDashboardApi(apmApiClient, serviceName, start, end).then((response) => { + const promises = response.body.serviceDashboards.map((item) => { + if (item.id) { + return apmApiClient.writeUser({ + endpoint: 'DELETE /internal/apm/custom-dashboard', + params: { query: { customDashboardId: item.id } }, + }); + } + }); + return Promise.all(promises); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/custom_dashboards/custom_dashboards.spec.ts b/x-pack/test/apm_api_integration/tests/custom_dashboards/custom_dashboards.spec.ts new file mode 100644 index 00000000000000..773e2bb06686d0 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/custom_dashboards/custom_dashboards.spec.ts @@ -0,0 +1,194 @@ +/* + * 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 { apm, timerange } from '@kbn/apm-synthtrace-client'; + +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + getServiceDashboardApi, + getLinkServiceDashboardApi, + deleteAllServiceDashboard, +} from './api_helper'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + const apmApiClient = getService('apmApiClient'); + const synthtrace = getService('synthtraceEsClient'); + + const start = '2023-08-22T00:00:00.000Z'; + const end = '2023-08-22T00:15:00.000Z'; + + registry.when( + 'Service dashboards when data is not loaded', + { config: 'basic', archives: [] }, + () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await getServiceDashboardApi(apmApiClient, 'synth-go', start, end); + expect(response.status).to.be(200); + expect(response.body.serviceDashboards).to.eql([]); + }); + }); + } + ); + + registry.when('Service dashboards when data is loaded', { config: 'basic', archives: [] }, () => { + const range = timerange(new Date(start).getTime(), new Date(end).getTime()); + + const goInstance = apm + .service({ + name: 'synth-go', + environment: 'production', + agentName: 'go', + }) + .instance('go-instance'); + + const javaInstance = apm + .service({ + name: 'synth-java', + environment: 'production', + agentName: 'java', + }) + .instance('java-instance'); + + before(async () => { + return synthtrace.index([ + range + .interval('1s') + .rate(4) + .generator((timestamp) => + goInstance + .transaction({ transactionName: 'GET /api' }) + .timestamp(timestamp) + .duration(1000) + .success() + ), + range + .interval('1s') + .rate(4) + .generator((timestamp) => + javaInstance + .transaction({ transactionName: 'GET /api' }) + .timestamp(timestamp) + .duration(1000) + .success() + ), + ]); + }); + + after(() => { + return synthtrace.clean(); + }); + + afterEach(async () => { + await deleteAllServiceDashboard(apmApiClient, 'synth-go', start, end); + }); + + describe('when data is not loaded', () => { + it('creates a new service dashboard', async () => { + const serviceDashboard = { + dashboardSavedObjectId: 'dashboard-saved-object-id', + serviceFiltersEnabled: true, + kuery: 'service.name: synth-go', + }; + const createResponse = await getLinkServiceDashboardApi({ + apmApiClient, + ...serviceDashboard, + }); + expect(createResponse.status).to.be(200); + expect(createResponse.body).to.have.property('id'); + expect(createResponse.body).to.have.property('updatedAt'); + + expect(createResponse.body).to.have.property( + 'dashboardSavedObjectId', + serviceDashboard.dashboardSavedObjectId + ); + expect(createResponse.body).to.have.property('kuery', serviceDashboard.kuery); + expect(createResponse.body).to.have.property( + 'serviceEnvironmentFilterEnabled', + serviceDashboard.serviceFiltersEnabled + ); + expect(createResponse.body).to.have.property( + 'serviceNameFilterEnabled', + serviceDashboard.serviceFiltersEnabled + ); + + const dasboardForGoService = await getServiceDashboardApi( + apmApiClient, + 'synth-go', + start, + end + ); + const dashboardForJavaService = await getServiceDashboardApi( + apmApiClient, + 'synth-java', + start, + end + ); + expect(dashboardForJavaService.body.serviceDashboards.length).to.be(0); + expect(dasboardForGoService.body.serviceDashboards.length).to.be(1); + }); + + it('updates the existing linked service dashboard', async () => { + const serviceDashboard = { + dashboardSavedObjectId: 'dashboard-saved-object-id', + serviceFiltersEnabled: true, + kuery: 'service.name: synth-go or agent.name: java', + }; + + await getLinkServiceDashboardApi({ + apmApiClient, + ...serviceDashboard, + }); + + const dasboardForGoService = await getServiceDashboardApi( + apmApiClient, + 'synth-go', + start, + end + ); + + const updateResponse = await getLinkServiceDashboardApi({ + apmApiClient, + customDashboardId: dasboardForGoService.body.serviceDashboards[0].id, + ...serviceDashboard, + serviceFiltersEnabled: true, + }); + + expect(updateResponse.status).to.be(200); + + const updateddasboardForGoService = await getServiceDashboardApi( + apmApiClient, + 'synth-go', + start, + end + ); + expect(updateddasboardForGoService.body.serviceDashboards.length).to.be(1); + expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property( + 'serviceEnvironmentFilterEnabled', + true + ); + expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property( + 'serviceNameFilterEnabled', + true + ); + expect(updateddasboardForGoService.body.serviceDashboards[0]).to.have.property( + 'kuery', + 'service.name: synth-go or agent.name: java' + ); + + const dashboardForJavaService = await getServiceDashboardApi( + apmApiClient, + 'synth-java', + start, + end + ); + expect(dashboardForJavaService.body.serviceDashboards.length).to.be(1); + }); + }); + }); +} From db009f19fcc2723d9fe78539071ee391d61d0bfc Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 3 Oct 2023 12:04:58 +0200 Subject: [PATCH 12/24] [Synthetics] Adds retesting on failure (#165626) Co-authored-by: Justin Kambic Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../synthetics/single_metric_config.ts | 10 ++- .../test_formula_metric_attribute.ts | 9 ++- .../common/constants/client_defaults.ts | 6 +- .../common/constants/monitor_defaults.ts | 1 + .../common/constants/monitor_management.ts | 1 + .../common/requests/get_certs_request_body.ts | 4 +- .../monitor_management/monitor_types.ts | 1 + .../monitor_types_project.ts | 1 + .../common/runtime_types/ping/ping.ts | 19 ++++- .../e2e/helpers/synthetics_runner.ts | 5 +- .../journeys/services/data/browser_docs.ts | 1 + .../journeys/services/data/sample_docs.ts | 2 + .../components/filter_status_button.tsx | 50 ++++++++++++ .../hooks/use_error_failed_tests.tsx | 4 +- .../hooks/use_find_my_killer_state.ts | 4 +- .../hooks/use_last_error_state.tsx | 4 +- .../monitor_add_edit/form/defaults.test.tsx | 1 + .../monitor_add_edit/form/field_config.tsx | 18 +++++ .../monitor_add_edit/form/form_config.tsx | 5 ++ .../components/monitor_add_edit/types.ts | 1 + .../hooks/use_monitor_errors.tsx | 4 +- .../hooks/use_monitor_pings.tsx | 15 +++- .../monitor_summary/status_filter.tsx | 42 ++++++++++ .../monitor_summary/test_runs_table.tsx | 68 +++++++++++----- .../test_runs_table_header.tsx | 33 +++++++- .../hooks/use_simple_run_once_monitors.ts | 4 +- .../synthetics/hooks/use_last_x_checks.ts | 4 +- .../hooks/use_status_by_location.tsx | 4 +- .../state/monitor_details/actions.ts | 18 ++--- .../synthetics/state/monitor_details/api.ts | 24 ++++-- .../synthetics/state/monitor_details/index.ts | 11 +++ .../state/monitor_details/selectors.ts | 6 ++ .../__mocks__/synthetics_store.mock.ts | 80 +++++++++++-------- x-pack/plugins/synthetics/scripts/base_e2e.js | 4 +- .../alert_rules/tls_rule/tls_rule_executor.ts | 4 +- .../server/common/pings/query_pings.ts | 2 + .../server/queries/query_monitor_status.ts | 4 +- .../routes/monitor_cruds/get_monitor.test.ts | 1 + .../routes/monitor_cruds/helper.test.ts | 1 + .../monitor_cruds/monitor_validation.test.ts | 2 + .../server/routes/pings/get_pings.ts | 9 ++- .../migrations/monitors/8.6.0.test.ts | 16 ++-- .../migrations/monitors/8.6.0.ts | 20 +++-- .../migrations/monitors/8.8.0.test.ts | 1 + .../migrations/monitors/8.8.0.ts | 18 +++-- .../migrations/monitors/8.9.0.ts | 16 ++-- .../monitors/test_fixtures/8.5.0.ts | 11 ++- .../monitors/test_fixtures/8.7.0.ts | 39 +++++---- .../saved_objects/synthetics_monitor.ts | 1 + .../formatters/formatting_utils.ts | 9 ++- .../private_formatters/common_formatters.ts | 1 + .../formatters/public_formatters/common.ts | 3 +- .../private_location/clean_up_task.ts | 2 +- .../normalizers/common_fields.test.ts | 2 + .../normalizers/common_fields.ts | 14 ++++ .../synthetics_service/utils/secrets.ts | 5 +- .../apis/synthetics/add_monitor_project.ts | 9 +++ .../synthetics/fixtures/browser_monitor.json | 3 +- .../synthetics/fixtures/http_monitor.json | 1 + .../synthetics/fixtures/icmp_monitor.json | 3 +- .../fixtures/inspect_browser_monitor.json | 3 +- .../fixtures/project_browser_monitor.json | 3 +- .../fixtures/project_http_monitor.json | 3 +- .../apis/synthetics/fixtures/tcp_monitor.json | 3 +- .../apis/synthetics/inspect_monitor.ts | 2 + .../apis/uptime/rest/helper/make_checks.ts | 1 + .../uptime/rest/monitor_states_real_data.ts | 33 +++----- 67 files changed, 514 insertions(+), 195 deletions(-) create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/filter_status_button.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/status_filter.tsx diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts index ed0df54219a29d..a90c111329ebee 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts @@ -70,7 +70,10 @@ export function getSyntheticsSingleMetricConfig({ dataView }: ConfigProps): Seri }, titlePosition: 'bottom', }, - columnFilter: { language: 'kuery', query: 'summary.up: *' }, + columnFilter: { + language: 'kuery', + query: 'summary.final_attempt: true or (not summary.final_attempt: * and summary:*)', + }, }, { id: 'monitor_duration', @@ -143,7 +146,10 @@ export function getSyntheticsSingleMetricConfig({ dataView }: ConfigProps): Seri }, field: RECORDS_FIELD, format: 'number', - columnFilter: { language: 'kuery', query: 'summary.down > 0' }, + columnFilter: { + language: 'kuery', + query: 'summary.status: down and summary.final_attempt: true', + }, }, ], labels: FieldLabels, diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/test_formula_metric_attribute.ts b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/test_formula_metric_attribute.ts index d1f63100ecbe85..444f50835e5303 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/test_formula_metric_attribute.ts +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/test_formula_metric_attribute.ts @@ -40,7 +40,8 @@ export const sampleMetricFormulaAttribute = { dataType: 'number', filter: { language: 'kuery', - query: 'summary.up: *', + query: + 'summary.final_attempt: true or (not summary.final_attempt: * and summary:*)', }, isBucketed: false, label: 'Availability', @@ -62,7 +63,8 @@ export const sampleMetricFormulaAttribute = { dataType: 'number', filter: { language: 'kuery', - query: '(summary.up: *) AND (summary.down > 0)', + query: + '(summary.final_attempt: true or (not summary.final_attempt: * and summary:*)) AND (summary.down > 0)', }, isBucketed: false, label: 'Part of Availability', @@ -78,7 +80,8 @@ export const sampleMetricFormulaAttribute = { dataType: 'number', filter: { language: 'kuery', - query: 'summary.up: *', + query: + 'summary.final_attempt: true or (not summary.final_attempt: * and summary:*)', }, isBucketed: false, label: 'Part of Availability', diff --git a/x-pack/plugins/synthetics/common/constants/client_defaults.ts b/x-pack/plugins/synthetics/common/constants/client_defaults.ts index 41a5f7c64abed3..c330a928d8f934 100644 --- a/x-pack/plugins/synthetics/common/constants/client_defaults.ts +++ b/x-pack/plugins/synthetics/common/constants/client_defaults.ts @@ -45,9 +45,9 @@ export const CLIENT_DEFAULTS = { }; export const EXCLUDE_RUN_ONCE_FILTER = { bool: { must_not: { exists: { field: 'run_once' } } } }; -export const SUMMARY_FILTER = { - exists: { - field: 'summary', +export const FINAL_SUMMARY_FILTER = { + term: { + 'summary.final_attempt': true, }, }; diff --git a/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts b/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts index 36f9fbf467dc09..e722801c12c1dd 100644 --- a/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts +++ b/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts @@ -149,6 +149,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = { [ConfigKey.CONFIG_HASH]: '', [ConfigKey.MONITOR_QUERY_ID]: '', [ConfigKey.PARAMS]: '', + [ConfigKey.MAX_ATTEMPTS]: 2, }; export const DEFAULT_BROWSER_ADVANCED_FIELDS: BrowserAdvancedFields = { diff --git a/x-pack/plugins/synthetics/common/constants/monitor_management.ts b/x-pack/plugins/synthetics/common/constants/monitor_management.ts index e87d05bc315218..a91e0132ff3760 100644 --- a/x-pack/plugins/synthetics/common/constants/monitor_management.ts +++ b/x-pack/plugins/synthetics/common/constants/monitor_management.ts @@ -76,6 +76,7 @@ export enum ConfigKey { USERNAME = 'username', WAIT = 'wait', MONITOR_QUERY_ID = 'id', + MAX_ATTEMPTS = 'max_attempts', } export const secretKeys = [ diff --git a/x-pack/plugins/synthetics/common/requests/get_certs_request_body.ts b/x-pack/plugins/synthetics/common/requests/get_certs_request_body.ts index f14137e4f77b31..f4535f2f85b729 100644 --- a/x-pack/plugins/synthetics/common/requests/get_certs_request_body.ts +++ b/x-pack/plugins/synthetics/common/requests/get_certs_request_body.ts @@ -7,7 +7,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import DateMath from '@kbn/datemath'; -import { EXCLUDE_RUN_ONCE_FILTER, SUMMARY_FILTER } from '../constants/client_defaults'; +import { EXCLUDE_RUN_ONCE_FILTER, FINAL_SUMMARY_FILTER } from '../constants/client_defaults'; import type { CertificatesResults } from '../../server/queries/get_certs'; import { CertResult, GetCertsParams, Ping } from '../runtime_types'; import { createEsQuery } from '../utils/es_search'; @@ -80,7 +80,7 @@ export const getCertsRequestBody = ({ } : {}), filter: [ - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, EXCLUDE_RUN_ONCE_FILTER, ...(filters ? [filters] : []), ...(monitorIds && monitorIds.length > 0 diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts index af2304acb9e24a..e109396965e7f6 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -63,6 +63,7 @@ export const CommonFieldsCodec = t.intersection([ [ConfigKey.LOCATIONS]: MonitorLocationsCodec, [ConfigKey.MONITOR_QUERY_ID]: t.string, [ConfigKey.CONFIG_ID]: t.string, + [ConfigKey.MAX_ATTEMPTS]: t.number, }), t.partial({ [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorTypeCodec, diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts index 4a2a1a97ed88ec..23e39660842fae 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts @@ -47,6 +47,7 @@ export const ProjectMonitorCodec = t.intersection([ wait: t.string, hash: t.string, namespace: t.string, + retestOnFailure: t.boolean, }), ]); diff --git a/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts b/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts index 51adff582c478f..47fc6f8ec18baa 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts @@ -112,6 +112,7 @@ export const MonitorType = t.intersection([ id: t.string, name: t.string, }), + origin: t.union([t.literal('ui'), t.literal('project')]), }), ]); @@ -143,6 +144,18 @@ export const UrlType = t.partial({ path: t.string, }); +const SummaryCodec = t.type({ + down: t.number, + up: t.number, + status: t.union([t.literal('up'), t.literal('down')]), + attempt: t.number, + max_attempts: t.number, + final_attempt: t.boolean, + retry_group: t.string, +}); + +export type TestSummary = t.TypeOf; + export const PingType = t.intersection([ t.type({ timestamp: t.string, @@ -205,10 +218,7 @@ export const PingType = t.intersection([ us: t.number, }), }), - summary: t.partial({ - down: t.number, - up: t.number, - }), + summary: SummaryCodec, synthetics: SyntheticsDataType, tags: t.array(t.string), tcp: t.partial({ @@ -292,6 +302,7 @@ export const GetPingsParamsType = t.intersection([ monitorId: t.string, sort: t.string, status: t.string, + finalAttempt: t.boolean, }), ]); diff --git a/x-pack/plugins/synthetics/e2e/helpers/synthetics_runner.ts b/x-pack/plugins/synthetics/e2e/helpers/synthetics_runner.ts index c097e214ae3eb4..97e0fe72d81d6c 100644 --- a/x-pack/plugins/synthetics/e2e/helpers/synthetics_runner.ts +++ b/x-pack/plugins/synthetics/e2e/helpers/synthetics_runner.ts @@ -107,13 +107,14 @@ export class SyntheticsRunner { } const { headless, match, pauseOnError } = this.params; const noOfRuns = process.env.NO_OF_RUNS ? Number(process.env.NO_OF_RUNS) : 1; + const CI = process.env.CI === 'true'; console.log(`Running ${noOfRuns} times`); let results: PromiseType> = {}; for (let i = 0; i < noOfRuns; i++) { results = await syntheticsRun({ params: { kibanaUrl: this.kibanaUrl, getService: this.getService }, playwrightOptions: { - headless, + headless: headless ?? !CI, testIdAttribute: 'data-test-subj', chromiumSandbox: false, timeout: 60 * 1000, @@ -126,7 +127,7 @@ export class SyntheticsRunner { }, }, match: match === 'undefined' ? '' : match, - pauseOnError, + pauseOnError: pauseOnError ?? !CI, screenshots: 'only-on-failure', reporter: TestReporter, }); diff --git a/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts b/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts index 22576524545c10..d3964692a38f01 100644 --- a/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts +++ b/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/browser_docs.ts @@ -29,6 +29,7 @@ export const journeySummary = ({ summary: { up: 1, down: 0, + final_attempt: true, }, test_run_id: testRunId ?? '07e339f4-4d56-4cdb-b314-96faacaee645', agent: { diff --git a/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/sample_docs.ts b/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/sample_docs.ts index f9a28c7eaa1bf3..62e2013ce0ff89 100644 --- a/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/sample_docs.ts +++ b/x-pack/plugins/synthetics/e2e/synthetics/journeys/services/data/sample_docs.ts @@ -29,6 +29,7 @@ export const getUpHit = ({ summary: { up: 1, down: 0, + final_attempt: true, }, tcp: { rtt: { @@ -193,6 +194,7 @@ export const firstDownHit = ({ summary: { up: 0, down: 1, + final_attempt: true, }, tcp: { rtt: { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/filter_status_button.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/filter_status_button.tsx new file mode 100644 index 00000000000000..7615f8aaa52a0b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/filter_status_button.tsx @@ -0,0 +1,50 @@ +/* + * 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 { EuiFilterButton, EuiButtonColor } from '@elastic/eui'; +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { selectStatusFilter, setStatusFilter } from '../../../state'; + +export interface FilterStatusButtonProps { + content: string | JSX.Element; + dataTestSubj: string; + isDisabled?: boolean; + value?: 'up' | 'down'; + withNext: boolean; +} + +export const FilterStatusButton = ({ + content, + dataTestSubj, + isDisabled, + value, + withNext, +}: FilterStatusButtonProps) => { + const statusFilter = useSelector(selectStatusFilter); + const dispatch = useDispatch(); + + const isActive = statusFilter === value; + let color: EuiButtonColor = 'text'; + if (isActive) { + color = value === 'up' ? 'success' : value === 'down' ? 'danger' : 'text'; + } + return ( + { + dispatch(setStatusFilter(statusFilter === value ? undefined : value)); + }} + withNext={withNext} + color={color} + > + {content} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_error_failed_tests.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_error_failed_tests.tsx index a61bbb9d2b6b09..eb5268c5060891 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_error_failed_tests.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_error_failed_tests.tsx @@ -11,7 +11,7 @@ import { useMemo } from 'react'; import { Ping } from '../../../../../../common/runtime_types'; import { EXCLUDE_RUN_ONCE_FILTER, - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, } from '../../../../../../common/constants/client_defaults'; import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; import { useSyntheticsRefreshContext } from '../../../contexts'; @@ -32,7 +32,7 @@ export function useErrorFailedTests() { query: { bool: { filter: [ - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, EXCLUDE_RUN_ONCE_FILTER, { term: { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_find_my_killer_state.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_find_my_killer_state.ts index 33f923c94d2bad..88e23cb6dcfae0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_find_my_killer_state.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_find_my_killer_state.ts @@ -11,7 +11,7 @@ import { useReduxEsSearch } from '../../../hooks/use_redux_es_search'; import { Ping } from '../../../../../../common/runtime_types'; import { EXCLUDE_RUN_ONCE_FILTER, - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, } from '../../../../../../common/constants/client_defaults'; import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; import { useSyntheticsRefreshContext } from '../../../contexts'; @@ -39,7 +39,7 @@ export function useFindMyKillerState() { query: { bool: { filter: [ - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, EXCLUDE_RUN_ONCE_FILTER, { term: { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_last_error_state.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_last_error_state.tsx index c0f100a3441ca4..d86b41f08f5bbf 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_last_error_state.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_last_error_state.tsx @@ -11,7 +11,7 @@ import { useReduxEsSearch } from '../../../hooks/use_redux_es_search'; import { Ping } from '../../../../../../common/runtime_types'; import { EXCLUDE_RUN_ONCE_FILTER, - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, } from '../../../../../../common/constants/client_defaults'; import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; import { useSyntheticsRefreshContext } from '../../../contexts'; @@ -32,7 +32,7 @@ export function useErrorFailedTests() { query: { bool: { filter: [ - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, EXCLUDE_RUN_ONCE_FILTER, { term: { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/defaults.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/defaults.test.tsx index c2090eb0f47b03..90652b609db6f7 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/defaults.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/defaults.test.tsx @@ -67,6 +67,7 @@ describe('defaults', () => { urls: '', id: '', config_id: '', + max_attempts: 2, } as SyntheticsMonitor; it('correctly formats monitor type to form type', () => { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx index 05860378fca341..e1e3b077dbd19f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx @@ -1581,4 +1581,22 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({ }, }), }, + [ConfigKey.MAX_ATTEMPTS]: { + fieldKey: ConfigKey.MAX_ATTEMPTS, + component: Switch, + controlled: true, + props: ({ setValue, field, trigger }): EuiSwitchProps => ({ + id: 'syntheticsMonitorConfigMaxAttempts', + label: i18n.translate('xpack.synthetics.monitorConfig.retest.label', { + defaultMessage: 'Enable retest on failure', + }), + checked: field?.value === 2, + onChange: async (event) => { + const isChecked = !!event.target.checked; + setValue(ConfigKey.MAX_ATTEMPTS, isChecked ? 2 : 1); + await trigger(ConfigKey.MAX_ATTEMPTS); + }, + 'data-test-subj': 'syntheticsEnableAttemptSwitch', + }), + }, }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx index 8e44f9727a5c3a..ea7653130759d8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx @@ -198,6 +198,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({ FIELD(readOnly)[ConfigKey.MAX_REDIRECTS], FIELD(readOnly)[ConfigKey.TIMEOUT], FIELD(readOnly)[ConfigKey.ENABLED], + FIELD(readOnly)[ConfigKey.MAX_ATTEMPTS], FIELD(readOnly)[AlertConfigKey.STATUS_ENABLED], FIELD(readOnly)[AlertConfigKey.TLS_ENABLED], ], @@ -218,6 +219,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({ FIELD(readOnly)[`${ConfigKey.SCHEDULE}.number`], FIELD(readOnly)[ConfigKey.TIMEOUT], FIELD(readOnly)[ConfigKey.ENABLED], + FIELD(readOnly)[ConfigKey.MAX_ATTEMPTS], FIELD(readOnly)[AlertConfigKey.STATUS_ENABLED], FIELD(readOnly)[AlertConfigKey.TLS_ENABLED], ], @@ -235,6 +237,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({ FIELD(readOnly)[ConfigKey.LOCATIONS], FIELD(readOnly)[`${ConfigKey.SCHEDULE}.number`], FIELD(readOnly)[ConfigKey.ENABLED], + FIELD(readOnly)[ConfigKey.MAX_ATTEMPTS], FIELD(readOnly)[AlertConfigKey.STATUS_ENABLED], ], step3: [FIELD(readOnly)['source.inline'], FIELD(readOnly)[ConfigKey.PARAMS]], @@ -261,6 +264,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({ FIELD(readOnly)[ConfigKey.LOCATIONS], FIELD(readOnly)[`${ConfigKey.SCHEDULE}.number`], FIELD(readOnly)[ConfigKey.ENABLED], + FIELD(readOnly)[ConfigKey.MAX_ATTEMPTS], FIELD(readOnly)[AlertConfigKey.STATUS_ENABLED], ], advanced: [ @@ -286,6 +290,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({ FIELD(readOnly)[ConfigKey.WAIT], FIELD(readOnly)[ConfigKey.TIMEOUT], FIELD(readOnly)[ConfigKey.ENABLED], + FIELD(readOnly)[ConfigKey.MAX_ATTEMPTS], FIELD(readOnly)[AlertConfigKey.STATUS_ENABLED], ], advanced: [DEFAULT_DATA_OPTIONS(readOnly), ICMP_ADVANCED(readOnly).requestConfig], diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts index d318fa878e9cf3..e7794160143961 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts @@ -163,4 +163,5 @@ export interface FieldMap { [ConfigKey.IGNORE_HTTPS_ERRORS]: FieldMeta; [ConfigKey.MODE]: FieldMeta; [ConfigKey.IPV4]: FieldMeta; + [ConfigKey.MAX_ATTEMPTS]: FieldMeta; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx index 969e98a21c36c1..69871de54c7afb 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx @@ -11,7 +11,7 @@ import { useSelectedLocation } from './use_selected_location'; import { Ping, PingState } from '../../../../../../common/runtime_types'; import { EXCLUDE_RUN_ONCE_FILTER, - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, } from '../../../../../../common/constants/client_defaults'; import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; import { useSyntheticsRefreshContext } from '../../../contexts'; @@ -37,7 +37,7 @@ export function useMonitorErrors(monitorIdArg?: string) { query: { bool: { filter: [ - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, EXCLUDE_RUN_ONCE_FILTER, { range: { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_pings.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_pings.tsx index 59460fa814969f..aad04f78f361c0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_pings.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_pings.tsx @@ -10,7 +10,12 @@ import { useDispatch, useSelector } from 'react-redux'; import { useSelectedMonitor } from './use_selected_monitor'; import { useSelectedLocation } from './use_selected_location'; -import { getMonitorRecentPingsAction, selectMonitorPingsMetadata } from '../../../state'; +import { + getMonitorRecentPingsAction, + selectMonitorPingsMetadata, + selectShowOnlyFinalAttempts, + selectStatusFilter, +} from '../../../state'; interface UseMonitorPingsProps { lastRefresh?: number; @@ -29,6 +34,10 @@ export const useMonitorPings = (props?: UseMonitorPingsProps) => { const monitorId = monitor?.id; const locationLabel = location?.label; + const showOnlyFinalAttempts = useSelector(selectShowOnlyFinalAttempts); + + const statusFilter = useSelector(selectStatusFilter); + useEffect(() => { if (monitorId && locationLabel) { dispatch( @@ -39,6 +48,8 @@ export const useMonitorPings = (props?: UseMonitorPingsProps) => { pageIndex: props?.pageIndex, from: props?.from, to: props?.to, + finalAttempt: showOnlyFinalAttempts, + statusFilter, }) ); } @@ -51,6 +62,8 @@ export const useMonitorPings = (props?: UseMonitorPingsProps) => { props?.pageIndex, props?.from, props?.to, + showOnlyFinalAttempts, + statusFilter, ]); const { total, data: pings, loading } = useSelector(selectMonitorPingsMetadata); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/status_filter.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/status_filter.tsx new file mode 100644 index 00000000000000..5dcd46d182cc49 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/status_filter.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFilterGroup } from '@elastic/eui'; +import { FilterStatusButton } from '../../common/components/filter_status_button'; +import { + STATUS_DOWN_LABEL, + STATUS_UP_LABEL, +} from '../../../../../../common/translations/translations'; + +export const StatusFilter: React.FC = () => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx index 0bbbd3a5f247b5..cf4e5532519ed3 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx @@ -15,12 +15,14 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, + EuiIconTip, EuiPanel, EuiText, useIsWithinMinBreakpoint, } from '@elastic/eui'; import { Criteria } from '@elastic/eui/src/components/basic_table/basic_table'; import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; +import { css } from '@kbn/kibana-react-plugin/common'; import { INSPECT_DOCUMENT, ViewDocument } from '../../common/components/view_document'; import { ExpandRowColumn, @@ -162,33 +164,53 @@ export const TestRunsTable = ({ ), }, }, + { + align: 'left', + valign: 'middle', + field: 'monitor.status', + name: RESULT_LABEL, + sortable: true, + render: (status: string, test: Ping) => { + const attemptNo = test.summary?.attempt ?? 1; + const isFinalAttempt = test.summary?.final_attempt ?? false; + if (!isFinalAttempt || attemptNo === 1) { + return ; + } + return ( + + + + + + + + + ); + }, + mobileOptions: { + show: false, + }, + }, ...(!isBrowserMonitor ? [ { align: 'left', field: 'monitor.ip', + sortable: true, name: i18n.translate('xpack.synthetics.pingList.ipAddressColumnLabel', { defaultMessage: 'IP', }), }, ] : []), - { - align: 'left', - valign: 'middle', - field: 'monitor.status', - name: RESULT_LABEL, - sortable: true, - render: (status: string) => , - mobileOptions: { - show: false, - }, - }, { align: 'left', field: 'error.message', name: MESSAGE_LABEL, textOnly: true, + css: css` + max-width: 600px; + `, render: (errorMessage: string) => ( {errorMessage?.length > 0 ? errorMessage : '-'} ), @@ -290,15 +312,7 @@ export const TestRunsTable = ({ columns={columns} error={pingsError?.body?.message} items={sortedPings} - noItemsMessage={ - pingsLoading - ? i18n.translate('xpack.synthetics.monitorDetails.loadingTestRuns', { - defaultMessage: 'Loading test runs...', - }) - : i18n.translate('xpack.synthetics.monitorDetails.noDataFound', { - defaultMessage: 'No data found', - }) - } + noItemsMessage={pingsLoading ? LOADING_TEST_RUNS : NO_DATA_FOUND} tableLayout={'auto'} sorting={sorting} onChange={handleTableChange} @@ -309,7 +323,7 @@ export const TestRunsTable = ({ pageIndex: page.index, pageSize: page.size, totalItemCount: total, - pageSizeOptions: [10, 20, 50], // TODO Confirm with Henry, + pageSizeOptions: [5, 10, 20, 50], } : undefined } @@ -400,6 +414,10 @@ const SCREENSHOT_LABEL = i18n.translate('xpack.synthetics.monitorDetails.summary defaultMessage: 'Screenshot', }); +const FINAL_ATTEMPT_LABEL = i18n.translate('xpack.synthetics.monitorDetails.summary.finalAttempt', { + defaultMessage: 'This is a retest since retry on failure is enabled.', +}); + const RESULT_LABEL = i18n.translate('xpack.synthetics.monitorDetails.summary.result', { defaultMessage: 'Result', }); @@ -411,3 +429,11 @@ const MESSAGE_LABEL = i18n.translate('xpack.synthetics.monitorDetails.summary.me const DURATION_LABEL = i18n.translate('xpack.synthetics.monitorDetails.summary.duration', { defaultMessage: 'Duration', }); + +const LOADING_TEST_RUNS = i18n.translate('xpack.synthetics.monitorDetails.loadingTestRuns', { + defaultMessage: 'Loading test runs...', +}); + +const NO_DATA_FOUND = i18n.translate('xpack.synthetics.monitorDetails.noDataFound', { + defaultMessage: 'No data found', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table_header.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table_header.tsx index 6785ed7f1ddc8d..3e43f23956180a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table_header.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table_header.tsx @@ -8,8 +8,18 @@ import React from 'react'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLink, EuiTitle } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiSwitch, + EuiTitle, +} from '@elastic/eui'; +import { useDispatch, useSelector } from 'react-redux'; +import { selectShowOnlyFinalAttempts, showOnlyFinalAttemptsAction } from '../../../state'; +import { StatusFilter } from './status_filter'; import { MONITOR_HISTORY_ROUTE } from '../../../../../../common/constants'; import { ConfigKey, Ping } from '../../../../../../common/runtime_types'; import { useGetUrlParams } from '../../../hooks'; @@ -33,14 +43,29 @@ export const TestRunsTableHeader = ({ const { monitor } = useSelectedMonitor(); + const showOnlyFinalAttempts = useSelector(selectShowOnlyFinalAttempts); + + const dispatch = useDispatch(); + return ( - +

      {paginable || pings?.length < 10 ? TEST_RUNS : LAST_10_TEST_RUNS}

      + + + + + dispatch(showOnlyFinalAttemptsAction(e.target.checked))} + /> + {showViewHistoryButton ? ( ({ query: { bool: { filter: [ - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, EXCLUDE_RUN_ONCE_FILTER, getTimeRangeFilter(schedule), { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_status_by_location.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_status_by_location.tsx index f3110999286a31..7d06f04130c05b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_status_by_location.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_status_by_location.tsx @@ -11,7 +11,7 @@ import { useLocations } from './use_locations'; import { EncryptedSyntheticsSavedMonitor, Ping } from '../../../../common/runtime_types'; import { EXCLUDE_RUN_ONCE_FILTER, - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, } from '../../../../common/constants/client_defaults'; import { SYNTHETICS_INDEX_PATTERN, UNNAMED_LOCATION } from '../../../../common/constants'; import { useSyntheticsRefreshContext } from '../contexts'; @@ -39,7 +39,7 @@ export function useStatusByLocation({ query: { bool: { filter: [ - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, EXCLUDE_RUN_ONCE_FILTER, { term: { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/actions.ts index 195c2b1fcb4008..b13233f5f22824 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/actions.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/actions.ts @@ -6,6 +6,7 @@ */ import { createAction } from '@reduxjs/toolkit'; +import { MostRecentPingsRequest } from './api'; import { Ping, PingsResponse, @@ -33,14 +34,9 @@ export const updateMonitorLastRunAction = createAction<{ data: Ping }>( '[MONITOR DETAILS] UPdATE LAST RUN' ); -export const getMonitorRecentPingsAction = createAsyncAction< - { - monitorId: string; - locationId: string; - size?: number; - pageIndex?: number; - from?: string; - to?: string; - }, - PingsResponse ->('[MONITOR DETAILS] GET RECENT PINGS'); +export const getMonitorRecentPingsAction = createAsyncAction( + '[MONITOR DETAILS] GET RECENT PINGS' +); + +export const showOnlyFinalAttemptsAction = createAction('SHOW ONLY FINAL ATTEMPTS'); +export const setStatusFilter = createAction<'up' | 'down' | undefined>('SET STATUS FILTER'); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/api.ts index 2f6e8d2df069e2..fb7953dba9cd6b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/api.ts @@ -25,6 +25,17 @@ export const fetchMonitorLastRun = async ({ return fetchMonitorRecentPings({ monitorId, locationId, size: 1 }); }; +export interface MostRecentPingsRequest { + monitorId: string; + locationId: string; + from?: string; + to?: string; + size?: number; + pageIndex?: number; + finalAttempt?: boolean; + statusFilter?: 'up' | 'down'; +} + export const fetchMonitorRecentPings = async ({ monitorId, locationId, @@ -32,14 +43,9 @@ export const fetchMonitorRecentPings = async ({ to, size = 10, pageIndex = 0, -}: { - monitorId: string; - locationId: string; - from?: string; - to?: string; - size?: number; - pageIndex?: number; -}): Promise => { + finalAttempt, + statusFilter, +}: MostRecentPingsRequest): Promise => { const locations = JSON.stringify([locationId]); const sort = 'desc'; @@ -53,6 +59,8 @@ export const fetchMonitorRecentPings = async ({ sort, size, pageIndex, + finalAttempt, + status: statusFilter, }, PingsResponseType ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts index af0ccf9046eea0..cf3fff3c428d7a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts @@ -19,6 +19,8 @@ import { getMonitorRecentPingsAction, setMonitorDetailsLocationAction, getMonitorAction, + showOnlyFinalAttemptsAction, + setStatusFilter, } from './actions'; export interface MonitorDetailsState { @@ -37,6 +39,8 @@ export interface MonitorDetailsState { syntheticsMonitorDispatchedAt: number; error: IHttpSerializedFetchError | null; selectedLocationId: string | null; + showOnlyFinalAttempts?: boolean; + statusFilter?: 'up' | 'down' | undefined; } const initialState: MonitorDetailsState = { @@ -47,6 +51,7 @@ const initialState: MonitorDetailsState = { syntheticsMonitorDispatchedAt: 0, error: null, selectedLocationId: null, + showOnlyFinalAttempts: false, }; export const monitorDetailsReducer = createReducer(initialState, (builder) => { @@ -110,6 +115,12 @@ export const monitorDetailsReducer = createReducer(initialState, (builder) => { if ('updated_at' in action.payload && state.syntheticsMonitor) { state.syntheticsMonitor = action.payload; } + }) + .addCase(showOnlyFinalAttemptsAction, (state, action) => { + state.showOnlyFinalAttempts = action.payload; + }) + .addCase(setStatusFilter, (state, action) => { + state.statusFilter = action.payload; }); }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/selectors.ts index b4aa80c558e8a3..c87e8b6775c6bb 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/selectors.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/selectors.ts @@ -25,3 +25,9 @@ export const selectPingsLoading = createSelector(getState, (state) => state.ping export const selectMonitorPingsMetadata = createSelector(getState, (state) => state.pings); export const selectPingsError = createSelector(getState, (state) => state.error); +export const selectShowOnlyFinalAttempts = createSelector( + getState, + (state) => state.showOnlyFinalAttempts ?? false +); + +export const selectStatusFilter = createSelector(getState, (state) => state.statusFilter); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts index 92149d84347fad..a57dcf8d758484 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts @@ -16,6 +16,7 @@ import { VerificationMode, TLSVersion, } from '../../../../../../common/runtime_types'; +import { MonitorDetailsState } from '../../../state'; /** * NOTE: This variable name MUST start with 'mock*' in order for @@ -199,7 +200,15 @@ function getMonitorDetailsMockSlice() { loading: false, loaded: true, data: { - summary: { up: 1, down: 0 }, + summary: { + up: 1, + down: 0, + status: 'up', + attempt: 1, + max_attempts: 1, + final_attempt: true, + retry_group: 'retry-1', + }, agent: { name: 'cron-b010e1cc9518984e-27644714-4pd4h', id: 'f8721d90-5aec-4815-a6f1-f4d4a6fb7482', @@ -257,7 +266,15 @@ function getMonitorDetailsMockSlice() { total: 3, data: [ { - summary: { up: 1, down: 0 }, + summary: { + up: 1, + down: 0, + status: 'up', + attempt: 1, + max_attempts: 1, + final_attempt: true, + retry_group: 'retry-1', + }, agent: { name: 'cron-b010e1cc9518984e-27644714-4pd4h', id: 'f8721d90-5aec-4815-a6f1-f4d4a6fb7482', @@ -266,7 +283,7 @@ function getMonitorDetailsMockSlice() { version: '8.3.0', }, synthetics: { - journey: { name: 'inline', id: 'inline', tags: null }, + journey: { name: 'inline', id: 'inline' }, type: 'heartbeat/summary', }, monitor: { @@ -300,18 +317,19 @@ function getMonitorDetailsMockSlice() { ecs: { version: '8.0.0' }, config_id: '4afd3980-0b72-11ed-9c10-b57918ea89d6', data_stream: { namespace: 'default', type: 'synthetics', dataset: 'browser' }, - 'event.type': 'journey/end', - event: { - agent_id_status: 'auth_metadata_missing', - ingested: '2022-07-24T17:14:07Z', - type: 'heartbeat/summary', - dataset: 'browser', - }, timestamp: '2022-07-24T17:14:05.079Z', docId: 'AkYzMYIBqL6WCtugsFck', }, { - summary: { up: 1, down: 0 }, + summary: { + up: 1, + down: 0, + status: 'up', + attempt: 1, + max_attempts: 1, + final_attempt: true, + retry_group: 'retry-1', + }, agent: { name: 'cron-b010e1cc9518984e-27644704-zs98t', id: 'a9620214-591d-48e7-9e5d-10b7a9fb1a03', @@ -354,18 +372,19 @@ function getMonitorDetailsMockSlice() { ecs: { version: '8.0.0' }, config_id: '4afd3980-0b72-11ed-9c10-b57918ea89d6', data_stream: { namespace: 'default', type: 'synthetics', dataset: 'browser' }, - 'event.type': 'journey/end', - event: { - agent_id_status: 'auth_metadata_missing', - ingested: '2022-07-24T17:04:06Z', - type: 'heartbeat/summary', - dataset: 'browser', - }, timestamp: '2022-07-24T17:04:03.769Z', docId: 'mkYqMYIBqL6WCtughFUq', }, { - summary: { up: 1, down: 0 }, + summary: { + up: 1, + down: 0, + status: 'up', + attempt: 1, + max_attempts: 1, + final_attempt: true, + retry_group: 'retry-1', + }, agent: { name: 'job-b010e1cc9518984e-dkw5k', id: 'e3a4e3a8-bdd1-44fe-86f5-e451b80f80c5', @@ -408,13 +427,6 @@ function getMonitorDetailsMockSlice() { ecs: { version: '8.0.0' }, config_id: '4afd3980-0b72-11ed-9c10-b57918ea89d6', data_stream: { namespace: 'default', type: 'synthetics', dataset: 'browser' }, - 'event.type': 'journey/end', - event: { - agent_id_status: 'auth_metadata_missing', - ingested: '2022-07-24T17:01:50Z', - type: 'heartbeat/summary', - dataset: 'browser', - }, timestamp: '2022-07-24T17:01:48.326Z', docId: 'kUYoMYIBqL6WCtugc1We', }, @@ -434,6 +446,7 @@ function getMonitorDetailsMockSlice() { locations: [{ isServiceManaged: true, id: 'us_central' }], namespace: 'default', origin: SourceType.UI, + max_attempts: 2, journey_id: '', project_id: '', playwright_options: '', @@ -475,7 +488,7 @@ function getMonitorDetailsMockSlice() { syntheticsMonitorDispatchedAt: 0, error: null, selectedLocationId: 'us_central', - }; + } as MonitorDetailsState; } function getPingStatusesMockSlice() { @@ -483,21 +496,22 @@ function getPingStatusesMockSlice() { return { pingStatuses: monitorDetails.pings.data.reduce((acc, cur) => { + const geoName = cur.observer.geo?.name!; if (!acc[cur.monitor.id]) { acc[cur.monitor.id] = {}; } - if (!acc[cur.monitor.id][cur.observer.geo.name]) { - acc[cur.monitor.id][cur.observer.geo.name] = {}; + if (!acc[cur.monitor.id][geoName]) { + acc[cur.monitor.id][geoName] = {}; } - acc[cur.monitor.id][cur.observer.geo.name][cur.timestamp] = { + acc[cur.monitor.id][geoName][cur.timestamp] = { timestamp: cur.timestamp, error: undefined, - locationId: cur.observer.geo.name, - config_id: cur.config_id, + locationId: geoName, + config_id: cur.config_id!, docId: cur.docId, - summary: cur.summary, + summary: cur.summary!, }; return acc; diff --git a/x-pack/plugins/synthetics/scripts/base_e2e.js b/x-pack/plugins/synthetics/scripts/base_e2e.js index 99fb58cf81d6a9..9cf6afcb2eb737 100644 --- a/x-pack/plugins/synthetics/scripts/base_e2e.js +++ b/x-pack/plugins/synthetics/scripts/base_e2e.js @@ -23,7 +23,7 @@ const { argv } = yargs(process.argv.slice(2)) 'Run all tests (an instance of Elasticsearch and kibana are needs to be available)', }) .option('pauseOnError', { - default: false, + default: !Boolean(process.env.CI), type: 'boolean', description: 'Pause the Synthetics Test Runner on error', }) @@ -33,7 +33,7 @@ const { argv } = yargs(process.argv.slice(2)) description: 'Path to the Kibana install directory', }) .option('headless', { - default: true, + default: Boolean(process.env.CI), type: 'boolean', description: 'Start in headless mode', }) diff --git a/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts index 0ca15a53efc0aa..e2f9f4c95cc63b 100644 --- a/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts +++ b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts @@ -11,7 +11,7 @@ import { import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import moment from 'moment'; -import { SUMMARY_FILTER } from '../../../common/constants/client_defaults'; +import { FINAL_SUMMARY_FILTER } from '../../../common/constants/client_defaults'; import { formatFilterString } from '../common'; import { SyntheticsServerSetup } from '../../types'; import { getSyntheticsCerts } from '../../queries/get_certs'; @@ -188,7 +188,7 @@ export class TLSRuleExecutor { config_id: configIds, }, }, - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, ], must_not: { bool: { diff --git a/x-pack/plugins/synthetics/server/common/pings/query_pings.ts b/x-pack/plugins/synthetics/server/common/pings/query_pings.ts index 23c4ce53578191..762233103e871b 100644 --- a/x-pack/plugins/synthetics/server/common/pings/query_pings.ts +++ b/x-pack/plugins/synthetics/server/common/pings/query_pings.ts @@ -94,6 +94,7 @@ export async function queryPings( pageIndex, locations, excludedLocations, + finalAttempt, } = params; const size = sizeParam ?? DEFAULT_PAGE_SIZE; @@ -107,6 +108,7 @@ export async function queryPings( { range: { '@timestamp': { gte: from, lte: to } } }, ...(monitorId ? [{ term: { 'monitor.id': monitorId } }] : []), ...(status ? [{ term: { 'monitor.status': status } }] : []), + ...(finalAttempt ? [{ term: { 'summary.final_attempt': finalAttempt } }] : []), ] as QueryDslQueryContainer[], ...REMOVE_NON_SUMMARY_BROWSER_CHECKS, }, diff --git a/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts b/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts index e18e4f23b34c8c..ddea241f3a3dfa 100644 --- a/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts +++ b/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts @@ -9,7 +9,7 @@ import pMap from 'p-map'; import times from 'lodash/times'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { cloneDeep, intersection } from 'lodash'; -import { SUMMARY_FILTER } from '../../common/constants/client_defaults'; +import { FINAL_SUMMARY_FILTER } from '../../common/constants/client_defaults'; import { OverviewPendingStatusMetaData, OverviewPing, @@ -69,7 +69,7 @@ export async function queryMonitorStatus( query: { bool: { filter: [ - SUMMARY_FILTER, + FINAL_SUMMARY_FILTER, { range: { '@timestamp': { diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.test.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.test.ts index f4393226323727..fa89e4aec11087 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.test.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.test.ts @@ -83,6 +83,7 @@ const attributes = { __ui: { is_tls_enabled: false }, urls: 'https://simonhearne.com/', max_redirects: '0', + max_attempts: 2, 'url.port': null, proxy_url: '', 'response.include_body': 'on_error', diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/helper.test.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/helper.test.ts index d083c3f688dd5c..73837e200f681f 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/helper.test.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/helper.test.ts @@ -108,6 +108,7 @@ const testMonitor = { }, urls: '${devUrl}', max_redirects: '0', + max_attempts: 2, 'url.port': null, proxy_url: '', 'response.include_body': 'on_error', diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts index 7c2498242b35cc..7d75a9644004d2 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts @@ -76,6 +76,7 @@ describe('validateMonitor', () => { ], [ConfigKey.NAMESPACE]: 'testnamespace', [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP, + [ConfigKey.MAX_ATTEMPTS]: 2, }; testMetaData = { is_tls_enabled: false, @@ -477,6 +478,7 @@ function getJsonPayload() { ' }' + ' },' + ' "max_redirects": "3",' + + ' "max_attempts": 2,' + ' "password": "test",' + ' "urls": "https://nextjs-test-synthetics.vercel.app/api/users",' + ' "proxy_url": "http://proxy.com",' + diff --git a/x-pack/plugins/synthetics/server/routes/pings/get_pings.ts b/x-pack/plugins/synthetics/server/routes/pings/get_pings.ts index 156a2e5dd62c84..8b464ef887a3c4 100644 --- a/x-pack/plugins/synthetics/server/routes/pings/get_pings.ts +++ b/x-pack/plugins/synthetics/server/routes/pings/get_pings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { SyntheticsRestApiRouteFactory } from '../types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { queryPings } from '../../common/pings/query_pings'; @@ -21,8 +21,11 @@ export const getPingsRouteQuerySchema = schema.object({ pageIndex: schema.maybe(schema.number()), sort: schema.maybe(schema.string()), status: schema.maybe(schema.string()), + finalAttempt: schema.maybe(schema.boolean()), }); +type GetPingsRouteRequest = TypeOf; + export const syntheticsGetPingsRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'GET', path: SYNTHETICS_API_URLS.PINGS, @@ -41,7 +44,8 @@ export const syntheticsGetPingsRoute: SyntheticsRestApiRouteFactory = () => ({ pageIndex, locations, excludedLocations, - } = request.query; + finalAttempt, + } = request.query as GetPingsRouteRequest; return await queryPings({ uptimeEsClient, @@ -54,6 +58,7 @@ export const syntheticsGetPingsRoute: SyntheticsRestApiRouteFactory = () => ({ pageIndex, locations: locations ? JSON.parse(locations) : [], excludedLocations, + finalAttempt, }); }, }); diff --git a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.test.ts b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.test.ts index 9caa803c77768f..d587ca26718d80 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.test.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.test.ts @@ -5,13 +5,9 @@ * 2.0. */ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; -import { migration860 } from './8.6.0'; +import { migration860, SyntheticsUnsanitizedDoc860 } from './8.6.0'; import { migrationMocks } from '@kbn/core/server/mocks'; -import { - ConfigKey, - LocationStatus, - SyntheticsMonitorWithSecretsAttributes, -} from '../../../../common/runtime_types'; +import { ConfigKey, LocationStatus } from '../../../../common/runtime_types'; const context = migrationMocks.createContext(); const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); @@ -53,10 +49,10 @@ const monitor850UI = { revision: 1, secrets: '{"password":"","check.request.body":{"type":"text","value":""},"check.request.headers":{},"check.response.body.negative":[],"check.response.body.positive":[],"check.response.headers":{},"ssl.key":"","ssl.key_passphrase":"","username":""}', - } as SyntheticsMonitorWithSecretsAttributes, + }, references: [], coreMigrationVersion: '8.5.0', -}; +} as SyntheticsUnsanitizedDoc860; const monitor850Project = { id: '3ab5c90f-aa7f-4370-ada2-b559191398f0', @@ -119,10 +115,10 @@ const monitor850Project = { revision: 1, secrets: '{"params":"{\\"url\\":\\"https://elastic.github.io/synthetics-demo/\\"}","source.inline.script":"","source.project.content":"UEsDBBQACAAIAAAAIQAAAAAAAAAAAAAAAAAXAAAAam91cm5leXMvb25lLmpvdXJuZXkudHNVkL1uwzAMhHc/BeFJAQyrLdAlQYouXbp3KjqwMhMrlUVVohMYgd+9in+AdCHED6c7kloDpkSy1R+JYtINd9bb356Mw/hDuqGzToOXlsSapIWSPOkT99HTkDR7qpemllTYLnAUuMLCKkhCoYKOvRWOMMIhcgflKzlM2e/OudwVZ4xgIqHQ+/wd9qA8drSB/QtcC1h96j6RuvUA0kYWcdYftzATgIYv3jE2W3h8qBbWh5k8r8DlGG+Gm2YiY67jZpfrMvuUXIG6QsBjfgSM2KWsWYeBaTlVOuy9aQFDcNagWPZllW86eAPqTgyAF7QyudVHFlazY91HN+Wu+bc67orPErNP+V1+1QeOb2hapXDy+3ejzLL+D1BLBwgqc7lrFgEAAMYBAABQSwECLQMUAAgACAAAACEAKnO5axYBAADGAQAAFwAAAAAAAAAAACAApIEAAAAAam91cm5leXMvb25lLmpvdXJuZXkudHNQSwUGAAAAAAEAAQBFAAAAWwEAAAAA","source.zip_url.username":"","source.zip_url.password":"","synthetics_args":[],"ssl.key":"","ssl.key_passphrase":""}', - } as SyntheticsMonitorWithSecretsAttributes, + }, references: [], coreMigrationVersion: '8.5.0', -}; +} as SyntheticsUnsanitizedDoc860; describe('Case migrations v8.5.0 -> v8.6.0', () => { beforeEach(() => { diff --git a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.ts b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.ts index 6eb6ee619f84c3..2cf529bdda5717 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.ts @@ -12,19 +12,23 @@ import { } from '../../../../common/runtime_types'; import { LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE } from '../../synthetics_monitor'; +export type SyntheticsMonitorWithSecretsAttributes860 = Omit< + SyntheticsMonitorWithSecretsAttributes, + ConfigKey.MAX_ATTEMPTS +>; + +export type SyntheticsUnsanitizedDoc860 = + SavedObjectUnsanitizedDoc; + export const migration860 = (encryptedSavedObjects: EncryptedSavedObjectsPluginSetup) => { return encryptedSavedObjects.createMigration< - SyntheticsMonitorWithSecretsAttributes, - SyntheticsMonitorWithSecretsAttributes + SyntheticsMonitorWithSecretsAttributes860, + SyntheticsMonitorWithSecretsAttributes860 >({ - isMigrationNeededPredicate: function shouldBeMigrated( - doc - ): doc is SavedObjectUnsanitizedDoc { + isMigrationNeededPredicate: function shouldBeMigrated(doc): doc is SyntheticsUnsanitizedDoc860 { return true; }, - migration: ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectUnsanitizedDoc => { + migration: (doc: SyntheticsUnsanitizedDoc860): SyntheticsUnsanitizedDoc860 => { const { attributes, id } = doc; return { ...doc, diff --git a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.test.ts b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.test.ts index 8f3b5e3c85a644..f816c2c2e2c837 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.test.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.test.ts @@ -137,6 +137,7 @@ describe('Monitor migrations v8.7.0 -> v8.8.0', () => { label: 'North America - US Central', }, ], + max_attempts: 2, name: 'https://elastic.co', namespace: 'default', origin: 'ui', diff --git a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.ts b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.ts index f71a742127c816..f165cee329f35d 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.ts @@ -33,20 +33,22 @@ import { normalizeMonitorSecretAttributes, } from '../../../synthetics_service/utils/secrets'; +export type SyntheticsMonitor880 = Omit< + SyntheticsMonitorWithSecretsAttributes, + ConfigKey.MAX_ATTEMPTS +>; + export const migration880 = (encryptedSavedObjects: EncryptedSavedObjectsPluginSetup) => { - return encryptedSavedObjects.createMigration< - SyntheticsMonitorWithSecretsAttributes, - SyntheticsMonitorWithSecretsAttributes - >({ + return encryptedSavedObjects.createMigration({ isMigrationNeededPredicate: function shouldBeMigrated( doc - ): doc is SavedObjectUnsanitizedDoc { + ): doc is SavedObjectUnsanitizedDoc { return true; }, migration: ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, logger - ): SavedObjectUnsanitizedDoc => { + ): SavedObjectUnsanitizedDoc => { let migrated = doc; migrated = { ...migrated, @@ -131,7 +133,7 @@ const omitZipUrlFields = (fields: BrowserFields) => { const updateThrottlingFields = ( doc: SavedObjectUnsanitizedDoc< - SyntheticsMonitorWithSecretsAttributes & + SyntheticsMonitor880 & Partial<{ [LegacyConfigKey.THROTTLING_CONFIG]: string; [LegacyConfigKey.IS_THROTTLING_ENABLED]: boolean; diff --git a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.9.0.ts b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.9.0.ts index c0c703233567fe..d0ee4859b8a885 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.9.0.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/8.9.0.ts @@ -12,19 +12,21 @@ import { } from '../../../../common/runtime_types'; import { SYNTHETICS_MONITOR_ENCRYPTED_TYPE } from '../../synthetics_monitor'; +export type SyntheticsMonitor890 = Omit< + SyntheticsMonitorWithSecretsAttributes, + ConfigKey.MAX_ATTEMPTS +>; + export const migration890 = (encryptedSavedObjects: EncryptedSavedObjectsPluginSetup) => { - return encryptedSavedObjects.createMigration< - SyntheticsMonitorWithSecretsAttributes, - SyntheticsMonitorWithSecretsAttributes - >({ + return encryptedSavedObjects.createMigration({ isMigrationNeededPredicate: function shouldBeMigrated( doc - ): doc is SavedObjectUnsanitizedDoc { + ): doc is SavedObjectUnsanitizedDoc { return true; }, migration: ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectUnsanitizedDoc => { + doc: SavedObjectUnsanitizedDoc + ): SavedObjectUnsanitizedDoc => { let migrated = doc; migrated = { ...migrated, diff --git a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/test_fixtures/8.5.0.ts b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/test_fixtures/8.5.0.ts index 265c082a2d6ed2..516511b722d6d1 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/test_fixtures/8.5.0.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/test_fixtures/8.5.0.ts @@ -5,7 +5,14 @@ * 2.0. */ import { SavedObjectUnsanitizedDoc } from '@kbn/core/server'; -import { SyntheticsMonitorWithSecretsAttributes } from '../../../../../common/runtime_types'; +import { + ConfigKey, + SyntheticsMonitorWithSecretsAttributes, +} from '../../../../../common/runtime_types'; + +export type SyntheticsSavedObjectUnsanitizedDoc850 = SavedObjectUnsanitizedDoc< + Omit +>; export const httpUI = { type: 'synthetics-monitor', @@ -46,4 +53,4 @@ export const httpUI = { coreMigrationVersion: '8.8.0', updated_at: '2023-04-11T17:42:11.734Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SyntheticsSavedObjectUnsanitizedDoc850; diff --git a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/test_fixtures/8.7.0.ts b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/test_fixtures/8.7.0.ts index 9cd401d8b3e202..c62da5c6bfd592 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/test_fixtures/8.7.0.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/migrations/monitors/test_fixtures/8.7.0.ts @@ -5,7 +5,18 @@ * 2.0. */ import { SavedObjectUnsanitizedDoc } from '@kbn/core/server'; -import { SyntheticsMonitorWithSecretsAttributes } from '../../../../../common/runtime_types'; +import { + ConfigKey, + SyntheticsMonitorWithSecretsAttributes, +} from '../../../../../common/runtime_types'; + +export type SyntheticsMonitorWithSecretsAttributes870 = Omit< + SyntheticsMonitorWithSecretsAttributes, + ConfigKey.MAX_ATTEMPTS +>; + +export type SavedObjectUnsanitizedDoc870 = + SavedObjectUnsanitizedDoc; export const browserUI = { type: 'synthetics-monitor', @@ -68,7 +79,7 @@ export const browserUI = { updated_at: '2023-03-31T20:31:24.177Z', created_at: '2023-03-31T20:31:24.177Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const browserSinglePageUI = { type: 'synthetics-monitor', id: '7a72e681-6033-444e-b402-bddbe4a9fc4e', @@ -123,7 +134,7 @@ export const browserSinglePageUI = { updated_at: '2023-03-31T20:32:01.498Z', created_at: '2023-03-31T20:32:01.498Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const httpUI = { type: 'synthetics-monitor', id: '8f4ad634-205b-440b-80c6-27aa6ef57bba', @@ -166,7 +177,7 @@ export const httpUI = { updated_at: '2023-03-31T20:32:14.362Z', created_at: '2023-03-31T20:32:14.362Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const tcpUI = { type: 'synthetics-monitor', id: 'b56a8fab-9a69-4435-b368-cc4fe6cdc6b0', @@ -205,7 +216,7 @@ export const tcpUI = { updated_at: '2023-03-31T20:32:27.678Z', created_at: '2023-03-31T20:32:27.678Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; const icmpUI = { type: 'synthetics-monitor', id: '1b625301-fe0b-46c0-9980-21347c58a6f8', @@ -236,7 +247,7 @@ const icmpUI = { updated_at: '2023-03-31T20:32:39.147Z', created_at: '2023-03-31T20:32:39.147Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const browserUptimeUI = { type: 'synthetics-monitor', id: '9bf12063-271f-47b1-9121-db1d14a71bb3', @@ -299,7 +310,7 @@ export const browserUptimeUI = { updated_at: '2023-03-31T20:35:34.916Z', created_at: '2023-03-31T20:35:34.916Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const tcpUptimeUI = { type: 'synthetics-monitor', id: '726d3f74-7760-4045-ad8d-87642403c721', @@ -345,7 +356,7 @@ export const tcpUptimeUI = { updated_at: '2023-03-31T20:38:29.582Z', created_at: '2023-03-31T20:38:29.582Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const httpUptimeUI = { type: 'synthetics-monitor', id: '35b2d765-4a62-4511-91c8-d5d52fdf4639', @@ -395,7 +406,7 @@ export const httpUptimeUI = { updated_at: '2023-03-31T20:37:24.093Z', created_at: '2023-03-31T20:37:24.093Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const icmpUptimeUI = { type: 'synthetics-monitor', id: '28b14c99-4a39-475d-9545-21b35b35751d', @@ -433,7 +444,7 @@ export const icmpUptimeUI = { updated_at: '2023-03-31T20:40:28.889Z', created_at: '2023-03-31T20:39:13.783Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const browserProject = { type: 'synthetics-monitor', id: 'ea123f46-eb02-4a8a-b3ce-53e645ce4aef', @@ -497,7 +508,7 @@ export const browserProject = { updated_at: '2023-03-31T20:43:35.214Z', created_at: '2023-03-31T20:43:35.214Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const httpProject = { type: 'synthetics-monitor', id: '316c0df8-56fc-428a-a477-7bf580f6cb4c', @@ -550,7 +561,7 @@ export const httpProject = { updated_at: '2023-03-31T20:43:35.214Z', created_at: '2023-03-31T20:43:35.214Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const icmpProject = { type: 'synthetics-monitor', id: 'e21a30b5-6d40-4458-8cff-9003d7b83eb6', @@ -591,7 +602,7 @@ export const icmpProject = { updated_at: '2023-03-31T20:43:35.214Z', created_at: '2023-03-31T20:43:35.214Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const tcpProject = { type: 'synthetics-monitor', id: '9f5d6206-9a1d-47fb-bd67-c7895b07f716', @@ -640,7 +651,7 @@ export const tcpProject = { updated_at: '2023-03-31T20:47:15.781Z', created_at: '2023-03-31T20:43:35.214Z', typeMigrationVersion: '8.6.0', -} as SavedObjectUnsanitizedDoc; +} as SavedObjectUnsanitizedDoc870; export const testMonitors = [ browserUI, diff --git a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor.ts b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor.ts index d206cd73285ea3..3a1ea76d671f55 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor.ts @@ -44,6 +44,7 @@ export const SYNTHETICS_MONITOR_ENCRYPTED_TYPE = { attributesToExcludeFromAAD: new Set([ ConfigKey.ALERT_CONFIG, ConfigKey.METADATA, + ConfigKey.MAX_ATTEMPTS, ...legacyConfigKeys, ]), }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/formatting_utils.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/formatting_utils.ts index 4eb566193e06a0..d06725b21421e2 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/formatting_utils.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/formatting_utils.ts @@ -10,7 +10,10 @@ import { ConfigKey, MonitorFields } from '../../../common/runtime_types'; import { ParsedVars, replaceVarsWithParams } from './lightweight_param_formatter'; import variableParser from './variable_parser'; -export type FormatterFn = (fields: Partial, key: ConfigKey) => string | null; +export type FormatterFn = ( + fields: Partial, + key: ConfigKey +) => string | number | null; export const replaceStringWithParams = ( value: string | boolean | {} | [], @@ -57,3 +60,7 @@ export const secondsToCronFormatter: FormatterFn = (fields, key) => { return value ? `${value}s` : null; }; + +export const maxAttemptsFormatter: FormatterFn = (fields, key) => { + return (fields[key] as number) ?? 2; +}; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/private_formatters/common_formatters.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/private_formatters/common_formatters.ts index b8d2687c34a8e0..ed55403a660b9d 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/private_formatters/common_formatters.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/private_formatters/common_formatters.ts @@ -36,6 +36,7 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.CONFIG_HASH]: null, [ConfigKey.MONITOR_QUERY_ID]: stringToJsonFormatter, [ConfigKey.PARAMS]: null, + [ConfigKey.MAX_ATTEMPTS]: null, [ConfigKey.SCHEDULE]: (fields) => JSON.stringify( `@every ${fields[ConfigKey.SCHEDULE]?.number}${fields[ConfigKey.SCHEDULE]?.unit}` diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/public_formatters/common.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/public_formatters/common.ts index ea0860646b495d..8eacb73a8d6c72 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/public_formatters/common.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/public_formatters/common.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { secondsToCronFormatter } from '../formatting_utils'; +import { maxAttemptsFormatter, secondsToCronFormatter } from '../formatting_utils'; import { arrayFormatter, stringToObjectFormatter } from './formatting_utils'; import { CommonFields, @@ -46,6 +46,7 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.ORIGINAL_SPACE]: null, [ConfigKey.CONFIG_HASH]: null, [ConfigKey.MONITOR_QUERY_ID]: null, + [ConfigKey.MAX_ATTEMPTS]: maxAttemptsFormatter, [ConfigKey.TIMEOUT]: secondsToCronFormatter, [ConfigKey.MONITOR_SOURCE_TYPE]: (fields) => fields[ConfigKey.MONITOR_SOURCE_TYPE] || SourceType.UI, diff --git a/x-pack/plugins/synthetics/server/synthetics_service/private_location/clean_up_task.ts b/x-pack/plugins/synthetics/server/synthetics_service/private_location/clean_up_task.ts index 7fdc4b7fa1605d..5bc47c17ef26e0 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/private_location/clean_up_task.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/private_location/clean_up_task.ts @@ -84,7 +84,7 @@ export const registerCleanUpTask = ( if (remaining.length === 0) { return { state, schedule: { interval: '24h' } }; } else { - return { state, schedule: { interval: '15m' } }; + return { state, schedule: { interval: '20m' } }; } } } catch (e) { diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts index f86c3097e809c9..6c381283c18245 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts @@ -136,6 +136,7 @@ describe('getNormalizeCommonFields', () => { tags: [], timeout: '16', params: '', + max_attempts: 2, }, }); } @@ -200,6 +201,7 @@ describe('getNormalizeCommonFields', () => { tags: [], timeout: '16', params: '', + max_attempts: 2, }, }); }); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts index 61509249e6ec1d..eba7d28aa4bd31 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts @@ -84,6 +84,7 @@ export const getNormalizeCommonFields = ({ ? getValueInSeconds(monitor.timeout) : defaultFields[ConfigKey.TIMEOUT], [ConfigKey.CONFIG_HASH]: monitor.hash || defaultFields[ConfigKey.CONFIG_HASH], + [ConfigKey.MAX_ATTEMPTS]: getMaxAttempts(monitor), [ConfigKey.PARAMS]: Object.keys(monitor.params || {}).length ? JSON.stringify(monitor.params) : defaultFields[ConfigKey.PARAMS], @@ -117,6 +118,19 @@ const getAlertConfig = (monitor: ProjectMonitor) => { : defaultFields[ConfigKey.ALERT_CONFIG]; }; +const ONLY_ONE_ATTEMPT = 1; + +const getMaxAttempts = (monitor: ProjectMonitor) => { + const defaultFields = DEFAULT_COMMON_FIELDS; + const retestOnFailure = monitor.retestOnFailure; + if (retestOnFailure) { + return defaultFields[ConfigKey.MAX_ATTEMPTS]; + } else if (monitor.retestOnFailure === false) { + return ONLY_ONE_ATTEMPT; + } + return defaultFields[ConfigKey.MAX_ATTEMPTS]; +}; + export const getCustomHeartbeatId = ( monitor: NormalizedProjectProps['monitor'], projectId: string, diff --git a/x-pack/plugins/synthetics/server/synthetics_service/utils/secrets.ts b/x-pack/plugins/synthetics/server/synthetics_service/utils/secrets.ts index a472811bafb272..d4be8612178aa0 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/utils/secrets.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/utils/secrets.ts @@ -6,6 +6,7 @@ */ import { omit, pick } from 'lodash'; import { SavedObject } from '@kbn/core/server'; +import { SyntheticsMonitor880 } from '../../saved_objects/migrations/monitors/8.8.0'; import { secretKeys } from '../../../common/constants/monitor_management'; import { ConfigKey, @@ -25,7 +26,7 @@ export function formatSecrets(monitor: SyntheticsMonitor): SyntheticsMonitorWith } export function normalizeSecrets( - monitor: SavedObject + monitor: SavedObject ): SavedObject { const attributes = normalizeMonitorSecretAttributes(monitor.attributes); const normalizedMonitor = { @@ -36,7 +37,7 @@ export function normalizeSecrets( } export function normalizeMonitorSecretAttributes( - monitor: SyntheticsMonitorWithSecretsAttributes + monitor: SyntheticsMonitorWithSecretsAttributes | SyntheticsMonitor880 ): SyntheticsMonitor { const defaultFields = DEFAULT_FIELDS[monitor[ConfigKey.MONITOR_TYPE]]; const normalizedMonitorAttributes = { diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts index 335550a2075535..8745be165a586a 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts @@ -229,6 +229,7 @@ export default function ({ getService }: FtrProviderContext) { urls: '', id: `${journeyId}-${project}-default`, hash: 'ekrjelkjrelkjre', + max_attempts: 2, }); } } finally { @@ -412,6 +413,7 @@ export default function ({ getService }: FtrProviderContext) { mode: 'any', ipv6: true, ipv4: true, + max_attempts: 2, }); } } finally { @@ -528,6 +530,7 @@ export default function ({ getService }: FtrProviderContext) { ipv6: true, ipv4: true, params: '', + max_attempts: 2, }); } } finally { @@ -640,6 +643,7 @@ export default function ({ getService }: FtrProviderContext) { ipv4: true, ipv6: true, params: '', + max_attempts: 2, }); } } finally { @@ -750,6 +754,7 @@ export default function ({ getService }: FtrProviderContext) { }, type: 'browser', hash: 'ekrjelkjrelkjre', + max_attempts: 2, }, reason: "Couldn't save or update monitor because of an invalid configuration.", }, @@ -1915,6 +1920,7 @@ export default function ({ getService }: FtrProviderContext) { testLocal1: 'testLocalParamsValue', }, proxy_url: '${testGlobalParam2}', + max_attempts: 2, }, reason: 'Cannot update monitor to different type.', }, @@ -2047,6 +2053,7 @@ export default function ({ getService }: FtrProviderContext) { testLocal1: 'testLocalParamsValue', }, proxy_url: '${testGlobalParam2}', + max_attempts: 2, }, reason: "Couldn't save or update monitor because of an invalid configuration.", }, @@ -2126,6 +2133,7 @@ export default function ({ getService }: FtrProviderContext) { testLocal1: 'testLocalParamsValue', }, proxy_url: '${testGlobalParam2}', + max_attempts: 2, }, reason: "Couldn't save or update monitor because of an invalid configuration.", }, @@ -2205,6 +2213,7 @@ export default function ({ getService }: FtrProviderContext) { testLocal1: 'testLocalParamsValue', }, proxy_url: '${testGlobalParam2}', + max_attempts: 2, }, reason: "Couldn't save or update monitor because of an invalid configuration.", }, diff --git a/x-pack/test/api_integration/apis/synthetics/fixtures/browser_monitor.json b/x-pack/test/api_integration/apis/synthetics/fixtures/browser_monitor.json index 44b1677de8a3ac..1ddcbfb8b6b8c8 100644 --- a/x-pack/test/api_integration/apis/synthetics/fixtures/browser_monitor.json +++ b/x-pack/test/api_integration/apis/synthetics/fixtures/browser_monitor.json @@ -55,5 +55,6 @@ "ssl.certificate_authorities": "", "ssl.supported_protocols": ["TLSv1.1", "TLSv1.2", "TLSv1.3"], "ssl.verification_mode": "full", - "revision": 1 + "revision": 1, + "max_attempts": 2 } diff --git a/x-pack/test/api_integration/apis/synthetics/fixtures/http_monitor.json b/x-pack/test/api_integration/apis/synthetics/fixtures/http_monitor.json index c97770b23da219..2968a1d58e7ffa 100644 --- a/x-pack/test/api_integration/apis/synthetics/fixtures/http_monitor.json +++ b/x-pack/test/api_integration/apis/synthetics/fixtures/http_monitor.json @@ -20,6 +20,7 @@ "__ui": { "is_tls_enabled": false }, + "max_attempts": 2, "max_redirects": "3", "password": "test", "urls": "https://nextjs-test-synthetics.vercel.app/api/users", diff --git a/x-pack/test/api_integration/apis/synthetics/fixtures/icmp_monitor.json b/x-pack/test/api_integration/apis/synthetics/fixtures/icmp_monitor.json index 590a74defb0938..51ad76e6f8c7de 100644 --- a/x-pack/test/api_integration/apis/synthetics/fixtures/icmp_monitor.json +++ b/x-pack/test/api_integration/apis/synthetics/fixtures/icmp_monitor.json @@ -30,5 +30,6 @@ "mode": "any", "ipv4": true, "ipv6": true, - "params": "" + "params": "", + "max_attempts": 2 } diff --git a/x-pack/test/api_integration/apis/synthetics/fixtures/inspect_browser_monitor.json b/x-pack/test/api_integration/apis/synthetics/fixtures/inspect_browser_monitor.json index 3a6bad21b45f4e..2589b867c902e9 100644 --- a/x-pack/test/api_integration/apis/synthetics/fixtures/inspect_browser_monitor.json +++ b/x-pack/test/api_integration/apis/synthetics/fixtures/inspect_browser_monitor.json @@ -80,5 +80,6 @@ }, "service": { "name": "" - } + }, + "max_attempts": 2 } diff --git a/x-pack/test/api_integration/apis/synthetics/fixtures/project_browser_monitor.json b/x-pack/test/api_integration/apis/synthetics/fixtures/project_browser_monitor.json index eb51f1fa4bf180..27fd108f9a8821 100644 --- a/x-pack/test/api_integration/apis/synthetics/fixtures/project_browser_monitor.json +++ b/x-pack/test/api_integration/apis/synthetics/fixtures/project_browser_monitor.json @@ -23,6 +23,7 @@ "filter": { "match": "check if title is present" }, - "hash": "ekrjelkjrelkjre" + "hash": "ekrjelkjrelkjre", + "max_attempts": 2 }] } diff --git a/x-pack/test/api_integration/apis/synthetics/fixtures/project_http_monitor.json b/x-pack/test/api_integration/apis/synthetics/fixtures/project_http_monitor.json index 7c9b9d45c2a40a..31e7717afbe409 100644 --- a/x-pack/test/api_integration/apis/synthetics/fixtures/project_http_monitor.json +++ b/x-pack/test/api_integration/apis/synthetics/fixtures/project_http_monitor.json @@ -79,7 +79,8 @@ "params": { "testLocal1": "testLocalParamsValue", "testGlobalParam2": "testGlobalParamOverwrite" - } + }, + "max_attempts": 2 } ] } diff --git a/x-pack/test/api_integration/apis/synthetics/fixtures/tcp_monitor.json b/x-pack/test/api_integration/apis/synthetics/fixtures/tcp_monitor.json index 8f92238e92a916..83511e533a5677 100644 --- a/x-pack/test/api_integration/apis/synthetics/fixtures/tcp_monitor.json +++ b/x-pack/test/api_integration/apis/synthetics/fixtures/tcp_monitor.json @@ -38,5 +38,6 @@ "mode": "any", "ipv4": true, "ipv6": true, - "params": "" + "params": "", + "max_attempts": 2 } diff --git a/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts b/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts index 59130c700ede72..947347fb2d4e41 100644 --- a/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/inspect_monitor.ts @@ -71,6 +71,7 @@ export default function ({ getService }: FtrProviderContext) { origin: 'ui', urls: 'https://nextjs-test-synthetics.vercel.app/api/users', max_redirects: '3', + max_attempts: 2, password: 'test', proxy_url: 'http://proxy.com', 'response.include_body': 'never', @@ -150,6 +151,7 @@ export default function ({ getService }: FtrProviderContext) { 'monitor.project.id': 'test-project-cb47c83a-45e7-416a-9301-cb476b5bff01', }, fields_under_root: true, + max_attempts: 2, }, ], }, diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts index 4f0b6daede4a38..9b5d1f2df1ce6c 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts @@ -139,6 +139,7 @@ export const makeChecksWithStatus = async ( if (d.summary) { d.summary[status] += d.summary[oppositeStatus]; d.summary[oppositeStatus] = 0; + d.summary.final_attempt = true; } return mogrify(d); diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts index 94ef6fdf034bb3..42ffd7665a339a 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts @@ -6,11 +6,7 @@ */ import expect from '@kbn/expect'; -import { isRight } from 'fp-ts/lib/Either'; -import { - MonitorSummariesResult, - MonitorSummariesResultType, -} from '@kbn/synthetics-plugin/common/runtime_types'; +import { MonitorSummariesResult } from '@kbn/synthetics-plugin/common/runtime_types'; import { API_URLS } from '@kbn/uptime-plugin/common/constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -36,22 +32,17 @@ const checkMonitorStatesResponse = ({ prevPagination, nextPagination, }: ExpectedMonitorStatesPage) => { - const decoded = MonitorSummariesResultType.decode(response); - expect(isRight(decoded)).to.be.ok(); - if (isRight(decoded)) { - const { summaries, prevPagePagination, nextPagePagination } = - decoded.right as MonitorSummariesResult; - expect(summaries).to.have.length(size); - expect(summaries?.map((s) => s.monitor_id)).to.eql(statesIds); - expect( - summaries?.map((s) => (s.state.summary?.up && !s.state.summary?.down ? 'up' : 'down')) - ).to.eql(statuses); - (summaries ?? []).forEach((s) => { - expect(s.state.url.full).to.be.ok(); - }); - expect(prevPagePagination).to.be(prevPagination); - expect(nextPagePagination).to.eql(nextPagination); - } + const { summaries, prevPagePagination, nextPagePagination } = response as MonitorSummariesResult; + expect(summaries).to.have.length(size); + expect(summaries?.map((s) => s.monitor_id)).to.eql(statesIds); + expect( + summaries?.map((s) => (s.state.summary?.up && !s.state.summary?.down ? 'up' : 'down')) + ).to.eql(statuses); + (summaries ?? []).forEach((s) => { + expect(s.state.url.full).to.be.ok(); + }); + expect(prevPagePagination).to.be(prevPagination); + expect(nextPagePagination).to.eql(nextPagination); }; export default function ({ getService }: FtrProviderContext) { From 40deb1345878d19ec06d1ada7ecd5f2fcfd9d799 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 3 Oct 2023 13:51:36 +0300 Subject: [PATCH 13/24] [Lens] Show icons/titles instead of previews in suggestions panel (#166808) ## Summary Use icons/titles for the suggestions panel instead of previews. We decided to move forward with this for performance reasons. Some suggestions can be very heavy and we have many sdhs from the customers complaining about it. It is not obvious to them that the performance problem is due to the suggestions. image The FTs changes are mostly changes to the selectors. --- .../kbn-journeys/services/page/kibana_page.ts | 7 ++ .../journeys/many_fields_lens_editor.ts | 4 +- .../editor_frame/editor_frame.tsx | 1 + .../editor_frame/suggestion_panel.scss | 1 + .../editor_frame/suggestion_panel.tsx | 7 +- .../visualizations/xy/xy_suggestions.test.ts | 4 +- .../visualizations/xy/xy_suggestions.ts | 73 ++++++++++++++----- .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - x-pack/test/accessibility/apps/lens.ts | 2 +- .../apps/lens/group3/epoch_millis.ts | 4 +- .../apps/lens/group5/drag_and_drop.ts | 9 ++- .../functional/apps/lens/group5/formula.ts | 2 +- .../apps/lens/open_in_lens/tsvb/dashboard.ts | 2 +- .../data_visualizer/index_data_visualizer.ts | 4 +- 16 files changed, 86 insertions(+), 46 deletions(-) diff --git a/packages/kbn-journeys/services/page/kibana_page.ts b/packages/kbn-journeys/services/page/kibana_page.ts index 974016dda0bc0d..8a45faf1a97cc2 100644 --- a/packages/kbn-journeys/services/page/kibana_page.ts +++ b/packages/kbn-journeys/services/page/kibana_page.ts @@ -104,6 +104,13 @@ export class KibanaPage { }); } + async waitForChartsSuggestions(count: number) { + await this.retry.waitFor(`rendering of ${count} suggestions is completed`, async () => { + const renderingItems = await this.page.$$('button[data-test-subj="lnsSuggestion"]'); + return renderingItems.length === count; + }); + } + async clearInput(locator: string) { const textArea = this.page.locator(locator); await textArea.clear(); diff --git a/x-pack/performance/journeys/many_fields_lens_editor.ts b/x-pack/performance/journeys/many_fields_lens_editor.ts index fa23315bac4f00..f14dbd17b6c233 100644 --- a/x-pack/performance/journeys/many_fields_lens_editor.ts +++ b/x-pack/performance/journeys/many_fields_lens_editor.ts @@ -26,6 +26,8 @@ export const journey = new Journey({ }) .step('Open existing Lens visualization', async ({ page, kibanaPage }) => { await page.click(subj('visListingTitleLink-Lens-Stress-Test')); + await page.waitForSelector(subj('lnsChartSwitchPopover')); - await kibanaPage.waitForCharts({ count: 6, timeout: 60000 }); + await kibanaPage.waitForCharts({ count: 1, timeout: 60000 }); + await kibanaPage.waitForChartsSuggestions(6); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index d68dbbf943b7e9..7d2d256d37c678 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -186,6 +186,7 @@ export function EditorFrame(props: EditorFrameProps) { getUserMessages={props.getUserMessages} nowProvider={props.plugins.data.nowProvider} core={props.core} + showOnlyIcons /> ) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.scss index 1619eea882b25a..f139bbe3ca1224 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.scss @@ -28,6 +28,7 @@ margin-right: $euiSizeS; margin-left: $euiSizeXS / 2; margin-bottom: $euiSizeXS / 2; + padding: 0 $euiSizeS; box-shadow: none !important; // sass-lint:disable-line no-important &:focus { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 6194d5ff33bc8c..51f0e3310ccdbc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -103,6 +103,7 @@ export interface SuggestionPanelProps { getUserMessages: UserMessagesGetter; nowProvider: DataPublicPluginStart['nowProvider']; core: CoreStart; + showOnlyIcons?: boolean; } const PreviewRenderer = ({ @@ -230,6 +231,7 @@ export function SuggestionPanel({ getUserMessages, nowProvider, core, + showOnlyIcons, }: SuggestionPanelProps) { const dispatchLens = useLensDispatch(); const activeDatasourceId = useLensSelector(selectActiveDatasourceId); @@ -437,7 +439,7 @@ export function SuggestionPanel({ onSuggestionRender(index + 1)} + showTitleAsLabel={showOnlyIcons} /> ); })} diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts index a12afc74655792..4d733d005dcf49 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts @@ -980,7 +980,7 @@ describe('xy_suggestions', () => { ], }); expect(seriesSuggestion.title).toEqual('Line chart'); - expect(stackSuggestion.title).toEqual('Stacked'); + expect(stackSuggestion.title).toEqual('Bar vertical stacked'); }); test('suggests a flipped chart for unchanged table and existing bar chart on ordinal x axis', () => { @@ -1053,7 +1053,7 @@ describe('xy_suggestions', () => { const visibleSuggestions = suggestions.filter((suggestion) => !suggestion.hide); expect(visibleSuggestions).toContainEqual( expect.objectContaining({ - title: 'Stacked', + title: 'Bar vertical stacked', state: expect.objectContaining({ preferredSeriesType: 'bar_stacked' }), }) ); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts index b63acd95133007..1d29cd32323fb5 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts @@ -295,30 +295,19 @@ function getSuggestionsForLayer({ buildSuggestion({ ...options, seriesType: newSeriesType, - title: newSeriesType.startsWith('bar') - ? i18n.translate('xpack.lens.xySuggestions.barChartTitle', { - defaultMessage: 'Bar chart', - }) - : i18n.translate('xpack.lens.xySuggestions.lineChartTitle', { - defaultMessage: 'Line chart', - }), + title: seriesTypeLabels(newSeriesType), }) ); } if (seriesType !== 'line' && splitBy && !seriesType.includes('percentage')) { // flip between stacked/unstacked + const suggestedSeriesType = toggleStackSeriesType(seriesType); sameStateSuggestions.push( buildSuggestion({ ...options, - seriesType: toggleStackSeriesType(seriesType), - title: seriesType.endsWith('stacked') - ? i18n.translate('xpack.lens.xySuggestions.unstackedChartTitle', { - defaultMessage: 'Unstacked', - }) - : i18n.translate('xpack.lens.xySuggestions.stackedChartTitle', { - defaultMessage: 'Stacked', - }), + seriesType: suggestedSeriesType, + title: seriesTypeLabels(suggestedSeriesType), }) ); } @@ -333,16 +322,15 @@ function getSuggestionsForLayer({ percentageOptions.splitBy = percentageOptions.xValue; delete percentageOptions.xValue; } + const suggestedSeriesType = asPercentageSeriesType(seriesType); // percentage suggestion sameStateSuggestions.push( buildSuggestion({ ...options, // hide the suggestion if split by is missing hide: !percentageOptions.splitBy, - seriesType: asPercentageSeriesType(seriesType), - title: i18n.translate('xpack.lens.xySuggestions.asPercentageTitle', { - defaultMessage: 'Percentage', - }), + seriesType: suggestedSeriesType, + title: seriesTypeLabels(suggestedSeriesType), }) ); } @@ -364,6 +352,53 @@ function getSuggestionsForLayer({ ); } +function seriesTypeLabels(seriesType: SeriesType) { + switch (seriesType) { + case 'line': + return i18n.translate('xpack.lens.xySuggestions.lineChartTitle', { + defaultMessage: 'Line chart', + }); + case 'area': + return i18n.translate('xpack.lens.xySuggestions.areaChartTitle', { + defaultMessage: 'Area chart', + }); + case 'area_stacked': + return i18n.translate('xpack.lens.xySuggestions.areaStackedChartTitle', { + defaultMessage: 'Area stacked', + }); + case 'area_percentage_stacked': + return i18n.translate('xpack.lens.xySuggestions.areaPercentageStackedChartTitle', { + defaultMessage: 'Area percentage', + }); + case 'bar': + return i18n.translate('xpack.lens.xySuggestions.verticalBarChartTitle', { + defaultMessage: 'Bar vertical', + }); + case 'bar_horizontal': + return i18n.translate('xpack.lens.xySuggestions.horizontalBarChartTitle', { + defaultMessage: 'Bar horizontal', + }); + case 'bar_stacked': + return i18n.translate('xpack.lens.xySuggestions.verticalBarStackedChartTitle', { + defaultMessage: 'Bar vertical stacked', + }); + case 'bar_horizontal_stacked': + return i18n.translate('xpack.lens.xySuggestions.horizontalBarStackedChartTitle', { + defaultMessage: 'Bar horizontal stacked', + }); + case 'bar_percentage_stacked': + return i18n.translate('xpack.lens.xySuggestions.verticalBarPercentageChartTitle', { + defaultMessage: 'Bar percentage', + }); + case 'bar_horizontal_percentage_stacked': + return i18n.translate('xpack.lens.xySuggestions.horizontalBarPercentageChartTitle', { + defaultMessage: 'Bar horizontal percentage', + }); + default: + return seriesType; + } +} + function toggleStackSeriesType(oldSeriesType: SeriesType) { switch (oldSeriesType) { case 'area': diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 04b8f1a36fc51b..4ca039b28c2a14 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -21420,13 +21420,9 @@ "xpack.lens.xyChart.verticalAxisLabel": "Axe vertical", "xpack.lens.xyChart.verticalLeftAxisLabel": "Axe gauche vertical", "xpack.lens.xyChart.verticalRightAxisLabel": "Axe droit vertical", - "xpack.lens.xySuggestions.asPercentageTitle": "Pourcentage", - "xpack.lens.xySuggestions.barChartTitle": "Graphique à barres", "xpack.lens.xySuggestions.emptyAxisTitle": "(vide)", "xpack.lens.xySuggestions.flipTitle": "Retourner", "xpack.lens.xySuggestions.lineChartTitle": "Graphique en courbes", - "xpack.lens.xySuggestions.stackedChartTitle": "Empilé", - "xpack.lens.xySuggestions.unstackedChartTitle": "Non empilé", "xpack.lens.xySuggestions.yAxixConjunctionSign": " & ", "xpack.lens.xyVisualization.areaLabel": "Aire", "xpack.lens.xyVisualization.barGroupLabel": "Barres", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 59adf32647cba0..7eab39e0ae1b26 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -21435,13 +21435,9 @@ "xpack.lens.xyChart.verticalAxisLabel": "縦軸", "xpack.lens.xyChart.verticalLeftAxisLabel": "縦左軸", "xpack.lens.xyChart.verticalRightAxisLabel": "縦右軸", - "xpack.lens.xySuggestions.asPercentageTitle": "割合(%)", - "xpack.lens.xySuggestions.barChartTitle": "棒グラフ", "xpack.lens.xySuggestions.emptyAxisTitle": "(空)", "xpack.lens.xySuggestions.flipTitle": "反転", "xpack.lens.xySuggestions.lineChartTitle": "折れ線グラフ", - "xpack.lens.xySuggestions.stackedChartTitle": "スタック", - "xpack.lens.xySuggestions.unstackedChartTitle": "スタックが解除されました", "xpack.lens.xySuggestions.yAxixConjunctionSign": " & ", "xpack.lens.xyVisualization.areaLabel": "エリア", "xpack.lens.xyVisualization.barGroupLabel": "棒", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 213e73960ce747..f9190f9a3c5a4e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -21435,13 +21435,9 @@ "xpack.lens.xyChart.verticalAxisLabel": "垂直轴", "xpack.lens.xyChart.verticalLeftAxisLabel": "垂直左轴", "xpack.lens.xyChart.verticalRightAxisLabel": "垂直右轴", - "xpack.lens.xySuggestions.asPercentageTitle": "百分比", - "xpack.lens.xySuggestions.barChartTitle": "条形图", "xpack.lens.xySuggestions.emptyAxisTitle": "(空)", "xpack.lens.xySuggestions.flipTitle": "翻转", "xpack.lens.xySuggestions.lineChartTitle": "折线图", - "xpack.lens.xySuggestions.stackedChartTitle": "堆叠", - "xpack.lens.xySuggestions.unstackedChartTitle": "非堆叠", "xpack.lens.xySuggestions.yAxixConjunctionSign": " & ", "xpack.lens.xyVisualization.areaLabel": "面积图", "xpack.lens.xyVisualization.barGroupLabel": "条形图", diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts index 4029cf77875514..1153d61d1fc68f 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/lens.ts @@ -138,7 +138,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'bytes', }); - await testSubjects.click('lnsSuggestion-barChart > lnsSuggestion'); + await testSubjects.click('lnsSuggestion-barVerticalStacked > lnsSuggestion'); await a11y.testAppSnapshot(); }); diff --git a/x-pack/test/functional/apps/lens/group3/epoch_millis.ts b/x-pack/test/functional/apps/lens/group3/epoch_millis.ts index 9096eaa1aab182..c13f31c97dc60d 100644 --- a/x-pack/test/functional/apps/lens/group3/epoch_millis.ts +++ b/x-pack/test/functional/apps/lens/group3/epoch_millis.ts @@ -43,7 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'count', field: 'Records', }); - await PageObjects.lens.waitForVisualization('legacyMtrVis'); + await PageObjects.lens.waitForVisualization('lnsSuggestion-countOfRecords'); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('1'); }); @@ -52,7 +52,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.enableTimeShift(); await PageObjects.lens.setTimeShift('3d'); - await PageObjects.lens.waitForVisualization('legacyMtrVis'); + await PageObjects.lens.waitForVisualization('lnsSuggestion-countOfRecords3D'); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('2'); }); }); diff --git a/x-pack/test/functional/apps/lens/group5/drag_and_drop.ts b/x-pack/test/functional/apps/lens/group5/drag_and_drop.ts index d6129d6c1a2d67..d1a449a6fa30bc 100644 --- a/x-pack/test/functional/apps/lens/group5/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/group5/drag_and_drop.ts @@ -359,11 +359,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.createLayer('data'); + // here the editor will error out as the mandatory vertical axis is missing await PageObjects.lens.dragDimensionToExtraDropType( 'lns-layerPanel-0 > lnsXY_xDimensionPanel > lns-dimensionTrigger', 'lns-layerPanel-1 > lnsXY_xDimensionPanel', 'duplicate', - xyChartContainer + 'workspace-error-message' ); await PageObjects.lens.assertFocusedDimension('@timestamp [1]'); @@ -446,11 +447,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'lns-layerPanel-1 > lnsXY_splitDimensionPanel > lns-empty-dimension' ); + // here the editor will error out as the mandatory vertical axis is missing await PageObjects.lens.dragDimensionToExtraDropType( 'lns-layerPanel-1 > lnsXY_splitDimensionPanel > lns-dimensionTrigger', 'lns-layerPanel-0 > lnsXY_splitDimensionPanel', 'swap', - xyChartContainer + 'workspace-error-message' ); expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ @@ -464,11 +466,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]); }); it('can combine dimensions', async () => { + // here the editor will error out as the mandatory vertical axis is missing await PageObjects.lens.dragDimensionToExtraDropType( 'lns-layerPanel-0 > lnsXY_splitDimensionPanel > lns-dimensionTrigger', 'lns-layerPanel-1 > lnsXY_splitDimensionPanel', 'combine', - xyChartContainer + 'workspace-error-message' ); expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ diff --git a/x-pack/test/functional/apps/lens/group5/formula.ts b/x-pack/test/functional/apps/lens/group5/formula.ts index 641cb4d120caa2..0bd1d662608cb7 100644 --- a/x-pack/test/functional/apps/lens/group5/formula.ts +++ b/x-pack/test/functional/apps/lens/group5/formula.ts @@ -174,7 +174,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'formula', }); - await PageObjects.lens.waitForVisualization('legacyMtrVis'); + await PageObjects.lens.waitForVisualization(); expect(await PageObjects.lens.getWorkspaceErrorCount()).to.eql(0); }); diff --git a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts index 9232860012bc9b..21e485ebe269c7 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts @@ -90,7 +90,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await panelActions.clickEdit(); await visualize.navigateToLensFromAnotherVisulization(); - await lens.waitForVisualization('legacyMtrVis'); + await lens.waitForVisualization('xyVisChart'); await retry.try(async () => { const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); expect(await dimensions[1].getVisibleText()).to.be('Count of records'); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index 794fad083be225..073023ed6b5dce 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -244,7 +244,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { if (lensMetricField) { await ml.dataVisualizerTable.assertLensActionShowChart( lensMetricField.fieldName, - 'legacyMtrVis' + 'xyVisChart' ); await ml.navigation.browserBackTo('dataVisualizerTableContainer'); } @@ -255,7 +255,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { if (lensNonMetricField) { await ml.dataVisualizerTable.assertLensActionShowChart( lensNonMetricField.fieldName, - 'legacyMtrVis' + 'xyVisChart' ); await ml.navigation.browserBackTo('dataVisualizerTableContainer'); } From 053507a2d2a7a94503e761b741fad5f26b55e997 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 3 Oct 2023 11:55:49 +0100 Subject: [PATCH 14/24] skip flaky suite (#166842) --- .../test_suites/security/ftr/cases/create_case_form.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/create_case_form.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/create_case_form.ts index 251af239b04e3b..14288c9c60668e 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/create_case_form.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/create_case_form.ts @@ -15,7 +15,8 @@ import { navigateToCasesApp } from '../../../../../shared/lib/cases'; const owner = SECURITY_SOLUTION_OWNER; export default ({ getService, getPageObject }: FtrProviderContext) => { - describe('Create Case', function () { + // FLAKY: https://github.com/elastic/kibana/issues/166842 + describe.skip('Create Case', function () { const find = getService('find'); const cases = getService('cases'); const testSubjects = getService('testSubjects'); From a9d9c898a8e465e2bb4ce6dcd525ad416a71b700 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 3 Oct 2023 11:58:22 +0100 Subject: [PATCH 15/24] skip flaky suite (#166597) --- .../functional/test_suites/search/navigation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test_serverless/functional/test_suites/search/navigation.ts b/x-pack/test_serverless/functional/test_suites/search/navigation.ts index 9b7e0d545394d2..26c2c7cc47ce85 100644 --- a/x-pack/test_serverless/functional/test_suites/search/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/search/navigation.ts @@ -16,7 +16,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const browser = getService('browser'); - describe('navigation', function () { + // FLAKY: https://github.com/elastic/kibana/issues/166597 + describe.skip('navigation', function () { before(async () => { await svlCommonPage.login(); await svlSearchNavigation.navigateToLandingPage(); From 1e0b8e4522bdf9f23f24da06b0eff14fab596c41 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 3 Oct 2023 12:00:37 +0100 Subject: [PATCH 16/24] skip flaky suite (#167189) --- .../observability/observability_log_explorer/header_menu.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts index f37d5e33c63aed..c7cba34e28ec2b 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'timePicker', ]); - describe('Header menu', () => { + // FLAKY: https://github.com/elastic/kibana/issues/167189 + describe.skip('Header menu', () => { before(async () => { await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await esArchiver.load( From 3098ca299ad54293b143d24b8c6d27bbc8348da2 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 3 Oct 2023 12:02:30 +0100 Subject: [PATCH 17/24] skip flaky suite (#166469) --- .../functional/test_suites/observability/cases/configure.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts index 91d5072a8162c3..a19ef4ebf2e4a5 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts @@ -31,7 +31,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await svlCommonPage.forceLogout(); }); - describe('Closure options', function () { + // FLAKY: https://github.com/elastic/kibana/issues/166469 + describe.skip('Closure options', function () { before(async () => { await common.clickAndValidate('configure-case-button', 'case-configure-title'); }); From 7fdd25184d08f8f980dc27d85101d80f0edd02f7 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 3 Oct 2023 12:08:42 +0100 Subject: [PATCH 18/24] skip flaky suite (#98240) --- test/api_integration/apis/ui_counters/ui_counters.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts index db75c82f293ac9..9d3bfa9c25b81f 100644 --- a/test/api_integration/apis/ui_counters/ui_counters.ts +++ b/test/api_integration/apis/ui_counters/ui_counters.ts @@ -56,7 +56,8 @@ export default function ({ getService }: FtrProviderContext) { return savedObject; }; - describe('UI Counters API', () => { + // FLAKY: https://github.com/elastic/kibana/issues/98240 + describe.skip('UI Counters API', () => { const dayDate = moment().format('DDMMYYYY'); before(async () => await esArchiver.emptyKibanaIndex()); From c4777d4d8825c62f1549784cd74bf8c6eb6f8e0f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 3 Oct 2023 12:10:20 +0100 Subject: [PATCH 19/24] skip flaky suite (#147020) --- .../classification_creation_saved_search.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts index 02c14a37277ec1..1444c13192e562 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts @@ -14,7 +14,8 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - describe('classification saved search creation', function () { + // FLAKY: https://github.com/elastic/kibana/issues/147020 + describe.skip('classification saved search creation', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote_small'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote_small', '@timestamp'); From dfeea506d54e49a264706bb24a671f5507458c7b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 3 Oct 2023 14:19:21 +0300 Subject: [PATCH 20/24] [Cases] Make Cases in the Stack Management GA (#167808) --- x-pack/plugins/cases/public/components/app/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/app/index.tsx b/x-pack/plugins/cases/public/components/app/index.tsx index f53e7edf9356ab..c5e4c87417a1b8 100644 --- a/x-pack/plugins/cases/public/components/app/index.tsx +++ b/x-pack/plugins/cases/public/components/app/index.tsx @@ -44,7 +44,6 @@ const CasesAppComponent: React.FC = ({ permissions: userCapabilities.generalCases, basePath: '/', features: { alerts: { enabled: false } }, - releasePhase: 'experimental', })} ); From b02f64bab9fb8acef6e6fefcd665846b8c17426a Mon Sep 17 00:00:00 2001 From: Jordan <51442161+JordanSh@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:23:14 +0300 Subject: [PATCH 21/24] [Cloud Security] Azure manual option - temporary solution (#167857) --- .../azure_credentials_form.tsx | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/azure_credentials_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/azure_credentials_form.tsx index 51ef9c7bcb955d..710ccdc124fea8 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/azure_credentials_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/azure_credentials_form.tsx @@ -68,11 +68,6 @@ const getSetupFormatOptions = (): CspRadioOption[] => [ label: i18n.translate('xpack.csp.azureIntegration.setupFormatOptions.manual', { defaultMessage: 'Manual', }), - disabled: true, - tooltip: i18n.translate( - 'xpack.csp.azureIntegration.setupFormatOptions.manual.disabledTooltip', - { defaultMessage: 'Coming Soon' } - ), }, ]; @@ -108,7 +103,7 @@ const ArmTemplateSetup = ({ return ( <> - +
        { + return ( + <> + + + + + ), + }} + /> + + + + + {i18n.translate('xpack.csp.azureIntegration.documentationLinkText', { + defaultMessage: 'documentation', + })} + + ), + }} + /> + + + ); +}; + const AZURE_MINIMUM_PACKAGE_VERSION = '1.6.0'; export const AzureCredentialsForm = ({ @@ -232,6 +271,9 @@ export const AzureCredentialsForm = ({ {setupFormat === AZURE_ARM_TEMPLATE_CREDENTIAL_TYPE && ( )} + {setupFormat === AZURE_MANUAL_CREDENTIAL_TYPE && ( + + )} ); From 8bf1ad5ef65354d9bf53a26a72817ca40a8e6ab2 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 3 Oct 2023 13:45:30 +0200 Subject: [PATCH 22/24] [APM] Add best practices for API testing (#167507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Søren Louv-Jansen --- x-pack/plugins/apm/dev_docs/testing.md | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/x-pack/plugins/apm/dev_docs/testing.md b/x-pack/plugins/apm/dev_docs/testing.md index 362ae0027d6d4d..7ce4ed34b9e617 100644 --- a/x-pack/plugins/apm/dev_docs/testing.md +++ b/x-pack/plugins/apm/dev_docs/testing.md @@ -1,5 +1,15 @@ # Testing +We've got three ways of testing our code: + +- Unit testing with Jest +- API testing +- End-to-end testing (with Cypress) + +API tests are usually preferred. They're stable and reasonably quick, and give a good approximation of real-world usage. +E2E testing is suitable for common and vital user journeys. They are however less stable than API tests. +Unit testing is a good approach if you have a very specific piece of code with lots of possibilities that you want to test. + ## Unit Tests (Jest) ``` @@ -126,11 +136,13 @@ diff --git a/x-pack/plugins/apm/scripts/test/README.md b/x-pack/plugins/apm/scri ## Serverless API tests #### Start server and run tests (single process) + ``` node scripts/functional_tests.js --config x-pack/test_serverless/api_integration/test_suites/observability/config.ts ``` #### Start server and run tests (separate processes) + ```sh # Start server node scripts/functional_tests_server.js --config x-pack/test_serverless/api_integration/test_suites/observability/config.ts @@ -154,3 +166,31 @@ All files with a .stories.tsx extension will be loaded. You can access the devel For end-to-end (e.g. agent -> apm server -> elasticsearch <- kibana) development and testing of Elastic APM please check the the [APM Integration Testing repository](https://github.com/elastic/apm-integration-testing). Data can also be generated using the [kbn-apm-synthtrace](../../../../packages/kbn-apm-synthtrace/README.md) CLI. + +## Best practices for API tests + +### 1. File structure: + +- **Endpoint-specific testing**: Each API endpoint should ideally be tested in an individual `*.spec.ts` file. This makes it easy to find tests, and works well with our general approach of having single-purpose API endpoints. +- **Directory structure**: Organize these files into feature-specific folders to make navigation easier. Each feature-specific folder can have multiple `*.spec.ts` files related to that particular feature. + +### 2. Data: + +- **Prefer Synthtrace**: Use Synthtrace for all new tests. It offers better control over data being fed into Elasticsearch, making it easier to verify calculated statistics than using Elasticsearch archives. +- **Migrating existing tests**: Aim to migrate existing tests that are based on Elasticsearch archives to Synthtrace. If for some reason Synthtrace isn't suitable, it's preferable to manually index documents rather than using ES archives. +- **Scenario management**: + - Prefer to keep the Synthtrace scenario in the same file. This makes it easier to see what's going on. + - If you do end up moving the Synthtrace scenario to another file because it gets too long, make sure the inputs are passed as parameters to a function. This keeps the information information in the test file and prevents the reader from navigating back and forth. + - Avoid re-using the same Synthtrace scenario across multiple files (in the same file it's mostly fine, but a test-specific Synthtrace scenario doesn't hurt). Re-using it will result in less specific scenarios, making it harder to write specific tests. The single scenario will grow unwieldy. It's akin to using ES archives. +- **ML**: For tests that require ML data, use the `createAndRunApmMlJob` helper function. This starts an ML job and returns only when it has completed, including any anomalies that are generated. +- **Alerting**: For tests that require alerting data, use the `createApmRule` and `waitForRuleStatus` helpers. `createApmRule` sets some defaults when creating a rule, and `waitForRuleStatus` only return when a certain status is matching your expectations. This allows you to e.g. wait until an alert fires or recovers after exceeding a threshold + +### 3. Scope of tests: + +- **Different configurations**: Tests can run for different configurations. This allows us to keep e.g. a test whether an endpoint correctly throws with a failed license check in the same file as one that tests the return values from the endpoint if a license check doesn't fail. +- **Specificity**: Make checks as detailed as possible. Avoid broad "has data" checks, especially when return values can be controlled by Synthtrace. Avoid using snapshot testing. +- **Error handling**: For API endpoints that might return specific error codes or messages, ensure there are tests covering those specific scenarios. + +### 4. Security and access control: + +- **User privileges**: For calling APIs use `apm.readUser` whenever possible. If the endpoint requires write privileges, use `apm.writeUser` or any of the other predefined roles, whichever apply. Don't use roles with higher access levels unless required. From 83b9302534c6200c63ab1be408ec1f59c6876027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Tue, 3 Oct 2023 14:20:30 +0200 Subject: [PATCH 23/24] [Serverless Tests] Use public headers for the public telemetry config endpoint (#167860) --- .../test_suites/search/telemetry/telemetry_config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test_serverless/api_integration/test_suites/search/telemetry/telemetry_config.ts b/x-pack/test_serverless/api_integration/test_suites/search/telemetry/telemetry_config.ts index 381c2aa0f5caed..8df4bae9df5a03 100644 --- a/x-pack/test_serverless/api_integration/test_suites/search/telemetry/telemetry_config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/search/telemetry/telemetry_config.ts @@ -25,7 +25,7 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext) it('GET should get the default config', async () => { await supertest .get('/api/telemetry/v2/config') - .set(svlCommonApi.getInternalRequestHeader()) + .set(svlCommonApi.getCommonRequestHeader()) .expect(200, baseConfig); }); @@ -39,7 +39,7 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext) await supertest .get('/api/telemetry/v2/config') - .set(svlCommonApi.getInternalRequestHeader()) + .set(svlCommonApi.getCommonRequestHeader()) .expect(200, { ...baseConfig, labels: { From 8d9e12a19bf02e2fb2ed2fda48a2b6fbcfa89be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Tue, 3 Oct 2023 14:33:13 +0200 Subject: [PATCH 24/24] [Enterprise Search] Update connector tiles (#167656) ## Summary - Update Integration Tiles - Update Select Connector Tiles - GitHub, OneDrive, Google Drive to native connectors Screenshot 2023-09-29 at 16 38 08 Screenshot 2023-09-29 at 16 38 13 Screenshot 2023-09-29 at 16 38 25 Screenshot 2023-09-29 at 16 38 44 Screenshot 2023-09-29 at 16 38 51 ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-doc-links/src/get_doc_links.ts | 4 + packages/kbn-doc-links/src/types.ts | 4 + packages/kbn-search-connectors/connectors.ts | 50 ++- .../types/native_connectors.ts | 425 ++++++++++++++++++ .../apis/custom_integration/integrations.ts | 2 +- .../search_index/connector/constants.ts | 28 ++ .../shared/doc_links/doc_links.ts | 12 + .../shared/icons/connector_icons.ts | 8 + .../public/assets/source_icons/outlook.svg | 39 ++ .../public/assets/source_icons/teams.svg | 28 ++ .../public/assets/source_icons/zoom.svg | 16 + .../enterprise_search/server/integrations.ts | 180 +++++--- .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 15 files changed, 737 insertions(+), 71 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index ca81f5554ded86..eb0863811ae8c8 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -141,6 +141,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { configuration: `${ENTERPRISE_SEARCH_DOCS}configuration.html`, connectors: `${ENTERPRISE_SEARCH_DOCS}connectors.html`, connectorsAzureBlobStorage: `${ENTERPRISE_SEARCH_DOCS}connectors-azure-blob.html`, + connectorsBox: `${ENTERPRISE_SEARCH_DOCS}connectors-box.html`, connectorsClients: `${ENTERPRISE_SEARCH_DOCS}connectors.html#connectors-build`, connectorsConfluence: `${ENTERPRISE_SEARCH_DOCS}connectors-confluence.html`, connectorsDropbox: `${ENTERPRISE_SEARCH_DOCS}connectors-dropbox.html`, @@ -157,6 +158,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { connectorsNetworkDrive: `${ENTERPRISE_SEARCH_DOCS}connectors-network-drive.html`, connectorsOneDrive: `${ENTERPRISE_SEARCH_DOCS}connectors-onedrive.html`, connectorsOracle: `${ENTERPRISE_SEARCH_DOCS}connectors-oracle.html`, + connectorsOutlook: `${ENTERPRISE_SEARCH_DOCS}connectors-outlook.html`, connectorsPostgreSQL: `${ENTERPRISE_SEARCH_DOCS}connectors-postgresql.html`, connectorsS3: `${ENTERPRISE_SEARCH_DOCS}connectors-s3.html`, connectorsSalesforce: `${ENTERPRISE_SEARCH_DOCS}connectors-salesforce.html`, @@ -164,7 +166,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { connectorsSharepoint: `${ENTERPRISE_SEARCH_DOCS}connectors-sharepoint.html`, connectorsSharepointOnline: `${ENTERPRISE_SEARCH_DOCS}connectors-sharepoint-online.html`, connectorsSlack: `${ENTERPRISE_SEARCH_DOCS}connectors-slack.html`, + connectorsTeams: `${ENTERPRISE_SEARCH_DOCS}connectors-teams.html`, connectorsWorkplaceSearch: `${ENTERPRISE_SEARCH_DOCS}workplace-search-connectors.html`, + connectorsZoom: `${ENTERPRISE_SEARCH_DOCS}connectors-zoom.html`, crawlerExtractionRules: `${ENTERPRISE_SEARCH_DOCS}crawler-extraction-rules.html`, crawlerManaging: `${ENTERPRISE_SEARCH_DOCS}crawler-managing.html`, crawlerOverview: `${ENTERPRISE_SEARCH_DOCS}crawler.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 39209b0675e9b3..8b1a8866b47d4a 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -122,6 +122,7 @@ export interface DocLinks { readonly configuration: string; readonly connectors: string; readonly connectorsAzureBlobStorage: string; + readonly connectorsBox: string; readonly connectorsClients: string; readonly connectorsConfluence: string; readonly connectorsContentExtraction: string; @@ -138,14 +139,17 @@ export interface DocLinks { readonly connectorsNetworkDrive: string; readonly connectorsOneDrive: string; readonly connectorsOracle: string; + readonly connectorsOutlook: string; readonly connectorsPostgreSQL: string; readonly connectorsS3: string; readonly connectorsSalesforce: string; readonly connectorsServiceNow: string; readonly connectorsSharepoint: string; readonly connectorsSharepointOnline: string; + readonly connectorsTeams: string; readonly connectorsSlack: string; readonly connectorsWorkplaceSearch: string; + readonly connectorsZoom: string; readonly crawlerExtractionRules: string; readonly crawlerManaging: string; readonly crawlerOverview: string; diff --git a/packages/kbn-search-connectors/connectors.ts b/packages/kbn-search-connectors/connectors.ts index 119de69a0c5c02..9673af86e14d9b 100644 --- a/packages/kbn-search-connectors/connectors.ts +++ b/packages/kbn-search-connectors/connectors.ts @@ -67,7 +67,7 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ { iconPath: 'github.svg', isBeta: true, - isNative: false, + isNative: true, keywords: ['github', 'cloud', 'connector'], name: i18n.translate('searchConnectors.content.nativeConnectors.github.name', { defaultMessage: 'GitHub & GitHub Enterprise Server', @@ -87,7 +87,7 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ { iconPath: 'google_drive.svg', isBeta: true, - isNative: false, + isNative: true, keywords: ['google', 'drive', 'connector'], name: i18n.translate('searchConnectors.content.nativeConnectors.googleDrive.name', { defaultMessage: 'Google Drive', @@ -201,7 +201,7 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ { iconPath: 'onedrive.svg', isBeta: true, - isNative: false, + isNative: true, keywords: ['network', 'drive', 'file', 'connector'], name: i18n.translate('searchConnectors.content.nativeConnectors.oneDrive.name', { defaultMessage: 'OneDrive', @@ -240,6 +240,50 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 'sharepoint_server', }, + { + iconPath: 'box.svg', + isBeta: false, + isNative: false, + isTechPreview: true, + keywords: ['cloud', 'box'], + name: i18n.translate('searchConnectors.content.nativeConnectors.box.name', { + defaultMessage: 'Box', + }), + serviceType: 'box', + }, + { + iconPath: 'outlook.svg', + isBeta: false, + isNative: false, + isTechPreview: true, + keywords: ['outlook', 'connector'], + name: i18n.translate('searchConnectors.content.nativeConnectors.outlook.name', { + defaultMessage: 'Outlook', + }), + serviceType: 'outlook', + }, + { + iconPath: 'teams.svg', + isBeta: false, + isNative: false, + isTechPreview: true, + keywords: ['teams', 'connector'], + name: i18n.translate('searchConnectors.content.nativeConnectors.teams.name', { + defaultMessage: 'Teams', + }), + serviceType: 'teams', + }, + { + iconPath: 'zoom.svg', + isBeta: false, + isNative: false, + isTechPreview: true, + keywords: ['zoom', 'connector'], + name: i18n.translate('searchConnectors.content.nativeConnectors.zoom.name', { + defaultMessage: 'Zoom', + }), + serviceType: 'zoom', + }, { iconPath: 'custom.svg', isBeta: true, diff --git a/packages/kbn-search-connectors/types/native_connectors.ts b/packages/kbn-search-connectors/types/native_connectors.ts index 1c4b0bb7e16948..a69036fc7c0e81 100644 --- a/packages/kbn-search-connectors/types/native_connectors.ts +++ b/packages/kbn-search-connectors/types/native_connectors.ts @@ -66,6 +66,21 @@ const USE_TEXT_EXTRACTION_SERVICE_TOOLTIP = i18n.translate( } ); +const ENABLE_DOCUMENT_LEVEL_SECURITY_LABEL = i18n.translate( + 'searchConnectors.nativeConnectors.enableDLS.label', + { + defaultMessage: 'Enable document level security', + } +); + +const ENABLE_DOCUMENT_LEVEL_SECURITY_TOOLTIP = i18n.translate( + 'searchConnectors.nativeConnectors.enableDLS.tooltip', + { + defaultMessage: + 'Document level security ensures identities and permissions set in Google Drive are maintained in Elasticsearch. This enables you to restrict and personalize read-access users and groups have to documents in this index. Access control syncs ensure this metadata is kept up to date in your Elasticsearch documents.', + } +); + const DATABASE_LABEL = i18n.translate('searchConnectors.nativeConnectors.databaseLabel', { defaultMessage: 'Database', }); @@ -576,6 +591,291 @@ export const NATIVE_CONNECTOR_DEFINITIONS: Record c.id === 'sample_data_all')).to.be.above( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts index 16ec9105810860..d2797ea0b2abda 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts @@ -22,6 +22,13 @@ export const CONNECTORS_DICT: Record = { externalDocsUrl: 'https://learn.microsoft.com/azure/storage/blobs/', icon: CONNECTOR_ICONS.azure_blob_storage, }, + box: { + docsUrl: docLinks.connectorsBox, + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.box, + platinumOnly: true, + }, confluence: { docsUrl: docLinks.connectorsConfluence, externalAuthDocsUrl: '', @@ -114,6 +121,13 @@ export const CONNECTORS_DICT: Record = { externalDocsUrl: 'https://docs.oracle.com/database/oracle/oracle-database/', icon: CONNECTOR_ICONS.oracle, }, + outlook: { + docsUrl: docLinks.connectorsOutlook, + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.outlook, + platinumOnly: true, + }, postgresql: { docsUrl: docLinks.connectorsPostgreSQL, externalAuthDocsUrl: 'https://www.postgresql.org/docs/15/auth-methods.html', @@ -160,6 +174,20 @@ export const CONNECTORS_DICT: Record = { icon: CONNECTOR_ICONS.slack, platinumOnly: true, }, + teams: { + docsUrl: docLinks.connectorsTeams, + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.teams, + platinumOnly: true, + }, + zoom: { + docsUrl: docLinks.connectorsZoom, + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.zoom, + platinumOnly: true, + }, }; export const CONNECTORS = CONNECTOR_DEFINITIONS.map((connector) => ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index 11e93c7af3a29d..f84be1ad660b3e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -65,6 +65,7 @@ class DocLinks { public cloudIndexManagement: string; public connectors: string; public connectorsAzureBlobStorage: string; + public connectorsBox: string; public connectorsClients: string; public connectorsConfluence: string; public connectorsContentExtraction: string; @@ -81,6 +82,7 @@ class DocLinks { public connectorsNetworkDrive: string; public connectorsOneDrive: string; public connectorsOracle: string; + public connectorsOutlook: string; public connectorsPostgreSQL: string; public connectorsS3: string; public connectorsSalesforce: string; @@ -88,6 +90,8 @@ class DocLinks { public connectorsSharepoint: string; public connectorsSharepointOnline: string; public connectorsSlack: string; + public connectorsTeams: string; + public connectorsZoom: string; public connectorsWorkplaceSearch: string; public consoleGuide: string; public crawlerExtractionRules: string; @@ -229,6 +233,7 @@ class DocLinks { this.cloudIndexManagement = ''; this.connectors = ''; this.connectorsAzureBlobStorage = ''; + this.connectorsBox = ''; this.connectorsConfluence = ''; this.connectorsContentExtraction = ''; this.connectorsClients = ''; @@ -245,6 +250,7 @@ class DocLinks { this.connectorsNetworkDrive = ''; this.connectorsOneDrive = ''; this.connectorsOracle = ''; + this.connectorsOutlook = ''; this.connectorsPostgreSQL = ''; this.connectorsS3 = ''; this.connectorsSalesforce = ''; @@ -252,6 +258,8 @@ class DocLinks { this.connectorsSharepoint = ''; this.connectorsSharepointOnline = ''; this.connectorsSlack = ''; + this.connectorsTeams = ''; + this.connectorsZoom = ''; this.connectorsWorkplaceSearch = ''; this.consoleGuide = ''; this.crawlerExtractionRules = ''; @@ -394,6 +402,7 @@ class DocLinks { this.cloudIndexManagement = docLinks.links.cloud.indexManagement; this.connectors = docLinks.links.enterpriseSearch.connectors; this.connectorsAzureBlobStorage = docLinks.links.enterpriseSearch.connectorsAzureBlobStorage; + this.connectorsBox = docLinks.links.enterpriseSearch.connectorsBox; this.connectorsConfluence = docLinks.links.enterpriseSearch.connectorsConfluence; this.connectorsContentExtraction = docLinks.links.enterpriseSearch.connectorsContentExtraction; this.connectorsClients = docLinks.links.enterpriseSearch.connectorsClients; @@ -410,6 +419,7 @@ class DocLinks { this.connectorsNative = docLinks.links.enterpriseSearch.connectorsNative; this.connectorsNetworkDrive = docLinks.links.enterpriseSearch.connectorsNetworkDrive; this.connectorsOracle = docLinks.links.enterpriseSearch.connectorsOracle; + this.connectorsOutlook = docLinks.links.enterpriseSearch.connectorsOutlook; this.connectorsPostgreSQL = docLinks.links.enterpriseSearch.connectorsPostgreSQL; this.connectorsS3 = docLinks.links.enterpriseSearch.connectorsS3; this.connectorsSalesforce = docLinks.links.enterpriseSearch.connectorsSalesforce; @@ -417,6 +427,8 @@ class DocLinks { this.connectorsSharepoint = docLinks.links.enterpriseSearch.connectorsSharepoint; this.connectorsSharepointOnline = docLinks.links.enterpriseSearch.connectorsSharepointOnline; this.connectorsSlack = docLinks.links.enterpriseSearch.connectorsSlack; + this.connectorsTeams = docLinks.links.enterpriseSearch.connectorsTeams; + this.connectorsZoom = docLinks.links.enterpriseSearch.connectorsZoom; this.connectorsWorkplaceSearch = docLinks.links.enterpriseSearch.connectorsWorkplaceSearch; this.consoleGuide = docLinks.links.console.guide; this.crawlerExtractionRules = docLinks.links.enterpriseSearch.crawlerExtractionRules; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts b/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts index ab3dc7a6cfb37d..357d5e7ce96ca1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts @@ -6,6 +6,7 @@ */ import azure_blob_storage from '../../../assets/source_icons/azure_blob_storage.svg'; +import box from '../../../assets/source_icons/box.svg'; import confluence_cloud from '../../../assets/source_icons/confluence_cloud.svg'; import custom from '../../../assets/source_icons/custom.svg'; import dropbox from '../../../assets/source_icons/dropbox.svg'; @@ -20,6 +21,7 @@ import mysql from '../../../assets/source_icons/mysql.svg'; import network_drive from '../../../assets/source_icons/network_drive.svg'; import onedrive from '../../../assets/source_icons/onedrive.svg'; import oracle from '../../../assets/source_icons/oracle.svg'; +import outlook from '../../../assets/source_icons/outlook.svg'; import postgresql from '../../../assets/source_icons/postgresql.svg'; import amazon_s3 from '../../../assets/source_icons/s3.svg'; import salesforce from '../../../assets/source_icons/salesforce.svg'; @@ -27,10 +29,13 @@ import servicenow from '../../../assets/source_icons/servicenow.svg'; import sharepoint from '../../../assets/source_icons/sharepoint.svg'; import sharepoint_online from '../../../assets/source_icons/sharepoint_online.svg'; import slack from '../../../assets/source_icons/slack.svg'; +import teams from '../../../assets/source_icons/teams.svg'; +import zoom from '../../../assets/source_icons/zoom.svg'; export const CONNECTOR_ICONS = { amazon_s3, azure_blob_storage, + box, confluence_cloud, custom, dropbox, @@ -45,10 +50,13 @@ export const CONNECTOR_ICONS = { network_drive, onedrive, oracle, + outlook, postgresql, salesforce, servicenow, sharepoint, sharepoint_online, slack, + teams, + zoom, }; diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg new file mode 100644 index 00000000000000..74d4c44a4a8209 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg new file mode 100644 index 00000000000000..e70e1b297d8368 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg new file mode 100644 index 00000000000000..910a0807eec8b0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts index cedbd4215a656f..22aa5a8e82375b 100644 --- a/x-pack/plugins/enterprise_search/server/integrations.ts +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -6,49 +6,11 @@ */ import type { HttpServiceSetup } from '@kbn/core/server'; -import type { IntegrationCategory } from '@kbn/custom-integrations-plugin/common'; import type { CustomIntegrationsPluginSetup } from '@kbn/custom-integrations-plugin/server'; import { i18n } from '@kbn/i18n'; import { ConfigType } from '.'; -interface WorkplaceSearchIntegration { - id: string; - title: string; - description: string; - categories: IntegrationCategory[]; - uiInternalPath?: string; -} - -const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ - { - id: 'box', - title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.boxName', { - defaultMessage: 'Box', - }), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.boxDescription', - { - defaultMessage: 'Search over your files and folders stored on Box with Workplace Search.', - } - ), - categories: ['enterprise_search', 'workplace_search_content_source'], - }, - { - id: 'zendesk', - title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName', { - defaultMessage: 'Zendesk', - }), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.zendeskDescription', - { - defaultMessage: 'Search over your tickets on Zendesk with Workplace Search.', - } - ), - categories: ['enterprise_search', 'workplace_search_content_source'], - }, -]; - export const registerEnterpriseSearchIntegrations = ( config: ConfigType, http: HttpServiceSetup, @@ -57,23 +19,6 @@ export const registerEnterpriseSearchIntegrations = ( ) => { const nativeSearchTag = config.hasNativeConnectors && isCloud ? ['native_search'] : []; if (config.canDeployEntSearch) { - workplaceSearchIntegrations.forEach((integration) => { - customIntegrations.registerCustomIntegration({ - uiInternalPath: `/app/enterprise_search/workplace_search/sources/add/${integration.id}`, - icons: [ - { - type: 'svg', - src: http.basePath.prepend( - `/plugins/enterpriseSearch/assets/source_icons/${integration.id}.svg` - ), - }, - ], - isBeta: false, - shipper: 'enterprise_search', - ...integration, - }); - }); - customIntegrations.registerCustomIntegration({ id: 'app_search_json', title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.jsonName', { @@ -939,5 +884,130 @@ export const registerEnterpriseSearchIntegrations = ( shipper: 'enterprise_search', isBeta: false, }); + + customIntegrations.registerCustomIntegration({ + id: 'outlook', + title: i18n.translate('xpack.enterpriseSearch.integrations.connectors.outlookTitle', { + defaultMessage: 'Outlook', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.integrations.connectors.outlookDescription', + { + defaultMessage: 'Search over your content on Outlook.', + } + ), + categories: [ + 'enterprise_search', + 'elastic_stack', + 'custom', + 'connector', + 'connector_client', + 'outlook', + ...nativeSearchTag, + ], + uiInternalPath: + '/app/enterprise_search/content/search_indices/new_index/connector?service_type=outlook', + icons: [ + { + type: 'svg', + src: http.basePath.prepend('/plugins/enterpriseSearch/assets/source_icons/outlook.svg'), + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + + customIntegrations.registerCustomIntegration({ + id: 'zoom', + title: i18n.translate('xpack.enterpriseSearch.integrations.connectors.zoomTitle', { + defaultMessage: 'Zoom', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.integrations.connectors.zoomDescription', + { + defaultMessage: 'Search over your content on Zoom.', + } + ), + categories: [ + 'enterprise_search', + 'elastic_stack', + 'custom', + 'connector', + 'connector_client', + 'zoom', + ...nativeSearchTag, + ], + uiInternalPath: + '/app/enterprise_search/content/search_indices/new_index/connector?service_type=zoom', + icons: [ + { + type: 'svg', + src: http.basePath.prepend('/plugins/enterpriseSearch/assets/source_icons/zoom.svg'), + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + + customIntegrations.registerCustomIntegration({ + id: 'teams', + title: i18n.translate('xpack.enterpriseSearch.integrations.connectors.teamsTitle', { + defaultMessage: 'Teams', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.integrations.connectors.teamsDescription', + { + defaultMessage: 'Search over your content on Teams.', + } + ), + categories: [ + 'enterprise_search', + 'elastic_stack', + 'custom', + 'connector', + 'connector_client', + 'teams', + ...nativeSearchTag, + ], + uiInternalPath: + '/app/enterprise_search/content/search_indices/new_index/connector?service_type=teams', + icons: [ + { + type: 'svg', + src: http.basePath.prepend('/plugins/enterpriseSearch/assets/source_icons/teams.svg'), + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + + customIntegrations.registerCustomIntegration({ + id: 'box', + title: i18n.translate('xpack.enterpriseSearch.integrations.connectors.boxTitle', { + defaultMessage: 'Box', + }), + description: i18n.translate('xpack.enterpriseSearch.integrations.connectors.boxDescription', { + defaultMessage: 'Search over your content on Box.', + }), + categories: [ + 'enterprise_search', + 'elastic_stack', + 'custom', + 'connector', + 'connector_client', + 'box', + ...nativeSearchTag, + ], + uiInternalPath: + '/app/enterprise_search/content/search_indices/new_index/connector?service_type=box', + icons: [ + { + type: 'svg', + src: http.basePath.prepend('/plugins/enterpriseSearch/assets/source_icons/box.svg'), + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); } }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 4ca039b28c2a14..19c63121a62b84 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -14715,8 +14715,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.sourcesModalLabel": "sources de contenu organisationnelles", "xpack.enterpriseSearch.workplaceSearch.integrations.azureBlob": "Stockage Blob Azure", "xpack.enterpriseSearch.workplaceSearch.integrations.azureBlobDescription": "Effectuez des recherches sur votre contenu sur Stockage Blob Azure avec Enterprise Search.", - "xpack.enterpriseSearch.workplaceSearch.integrations.boxDescription": "Effectuez des recherches dans vos fichiers et dossiers stockés sur Box avec Workplace Search.", - "xpack.enterpriseSearch.workplaceSearch.integrations.boxName": "Box", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloud": "Google Cloud Storage", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloudDescription": "Effectuez des recherches sur votre contenu sur Google Cloud Storage avec Enterprise Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.googleDriveDescription": "Effectuez des recherches dans vos documents sur Google Drive avec Workplace Search.", @@ -14739,8 +14737,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "SharePoint Online", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "Effectuez des recherches dans vos fichiers stockés sur le serveur Microsoft SharePoint avec Workplace Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerName": "Serveur SharePoint", - "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskDescription": "Effectuez des recherches dans vos tickets sur Zendesk avec Workplace Search.", - "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName": "Zendesk", "xpack.enterpriseSearch.workplaceSearch.keepEditing.button": "Continuer la modification", "xpack.enterpriseSearch.workplaceSearch.label.label": "Étiquette", "xpack.enterpriseSearch.workplaceSearch.name.label": "Nom", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7eab39e0ae1b26..89c3b1a590be58 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14729,8 +14729,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.sourcesModalLabel": "組織コンテンツソース", "xpack.enterpriseSearch.workplaceSearch.integrations.azureBlob": "Azure Blob Storage", "xpack.enterpriseSearch.workplaceSearch.integrations.azureBlobDescription": "エンタープライズ サーチでAzure Blob Storageのコンテンツを検索します。", - "xpack.enterpriseSearch.workplaceSearch.integrations.boxDescription": "Workplace Searchを使用して、Boxに保存されたファイルとフォルダーを検索します。", - "xpack.enterpriseSearch.workplaceSearch.integrations.boxName": "Box", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloud": "Google Cloud Storage", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloudDescription": "エンタープライズ サーチでGoogle Cloud Storageのコンテンツを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.googleDriveDescription": "Workplace Searchを使用して、Google Driveのドキュメントを検索します。", @@ -14753,8 +14751,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "SharePoint Online", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "Workplace Searchを使用して、Microsoft SharePoint Serverに保存されたファイルを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerName": "SharePoint Server", - "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskDescription": "Workplace Searchを使用して、Zendeskのチケットを検索します。", - "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName": "Zendesk", "xpack.enterpriseSearch.workplaceSearch.keepEditing.button": "編集を続行", "xpack.enterpriseSearch.workplaceSearch.label.label": "ラベル", "xpack.enterpriseSearch.workplaceSearch.name.label": "名前", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f9190f9a3c5a4e..46482145dcf97c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14729,8 +14729,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.sourcesModalLabel": "组织内容源", "xpack.enterpriseSearch.workplaceSearch.integrations.azureBlob": "Azure Blob 存储", "xpack.enterpriseSearch.workplaceSearch.integrations.azureBlobDescription": "使用 Enterprise Search 在 Azure Blob 存储上搜索您的内容。", - "xpack.enterpriseSearch.workplaceSearch.integrations.boxDescription": "通过 Workplace Search 搜索存储在 Box 上的文件和文件夹。", - "xpack.enterpriseSearch.workplaceSearch.integrations.boxName": "Box", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloud": "Google Cloud Storage", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloudDescription": "使用 Enterprise Search 在 Google Cloud Storage 上搜索您的内容。", "xpack.enterpriseSearch.workplaceSearch.integrations.googleDriveDescription": "通过 Workplace Search 搜索 Google 云端硬盘上的文档。", @@ -14753,8 +14751,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "Sharepoint", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "通过 Workplace Search 搜索存储在 Microsoft SharePoint Server 上的文件。", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerName": "SharePoint Server", - "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskDescription": "通过 Workplace Search 搜索 Zendesk 上的工单。", - "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName": "Zendesk", "xpack.enterpriseSearch.workplaceSearch.keepEditing.button": "继续编辑", "xpack.enterpriseSearch.workplaceSearch.label.label": "标签", "xpack.enterpriseSearch.workplaceSearch.name.label": "名称",