From 4395cdd1a0f5645b0c2ce504516b48c39ea41ae5 Mon Sep 17 00:00:00 2001
From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
Date: Wed, 10 Apr 2024 11:36:08 +0200
Subject: [PATCH 01/55] [Fleet] removed settings from Agent policy settings
config that are not implemented in agent (#180382)
## Summary
Remove settings that don't work in agent:
https://github.com/elastic/ingest-dev/issues/2471#issuecomment-2045256588
Only keeping `go_max_procs` for now, as that's the only one that seems
to work.
When changing in agent policy, I could see in agent logs:
`[elastic_agent][debug] agent limits have changed: {GoMaxProcs:1} ->
{GoMaxProcs:0}`
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../common/settings/agent_policy_settings.ts | 60 -------------------
.../agent_policies/full_agent_policy.test.ts | 8 ---
2 files changed, 68 deletions(-)
diff --git a/x-pack/plugins/fleet/common/settings/agent_policy_settings.ts b/x-pack/plugins/fleet/common/settings/agent_policy_settings.ts
index 7b170d330e650..f0d7a84d36d2a 100644
--- a/x-pack/plugins/fleet/common/settings/agent_policy_settings.ts
+++ b/x-pack/plugins/fleet/common/settings/agent_policy_settings.ts
@@ -37,64 +37,4 @@ export const AGENT_POLICY_ADVANCED_SETTINGS: SettingsConfig[] = [
},
schema: z.number().int().min(0).default(0),
},
- {
- name: 'agent.download.timeout',
- title: i18n.translate('xpack.fleet.settings.agentPolicyAdvanced.downloadTimeoutTitle', {
- defaultMessage: 'Agent binary download timeout',
- }),
- description: i18n.translate(
- 'xpack.fleet.settings.agentPolicyAdvanced.downloadTimeoutDescription',
- {
- defaultMessage: 'Timeout in seconds for downloading the agent binary',
- }
- ),
- learnMoreLink:
- 'https://www.elastic.co/guide/en/fleet/current/enable-custom-policy-settings.html#configure-agent-download-timeout',
- api_field: {
- name: 'agent_download_timeout',
- },
- schema: zodStringWithDurationValidation.default('120s'),
- },
- {
- name: 'agent.download.target_directory',
- api_field: {
- name: 'agent_download_target_directory',
- },
- title: i18n.translate(
- 'xpack.fleet.settings.agentPolicyAdvanced.agentDownloadTargetDirectoryTitle',
- {
- defaultMessage: 'Agent binary target directory',
- }
- ),
- description: i18n.translate(
- 'xpack.fleet.settings.agentPolicyAdvanced.agentDownloadTargetDirectoryDescription',
- {
- defaultMessage: 'The disk path to which the agent binary will be downloaded',
- }
- ),
- learnMoreLink:
- 'https://www.elastic.co/guide/en/fleet/current/elastic-agent-standalone-download.html',
- schema: z.string(),
- },
- {
- name: 'agent.logging.metrics.period',
- api_field: {
- name: 'agent_logging_metrics_period',
- },
- title: i18n.translate(
- 'xpack.fleet.settings.agentPolicyAdvanced.agentLoggingMetricsPeriodTitle',
- {
- defaultMessage: 'Agent logging metrics period',
- }
- ),
- description: i18n.translate(
- 'xpack.fleet.settings.agentPolicyAdvanced.agentLoggingMetricsPeriodDescription',
- {
- defaultMessage: 'The frequency of agent metrics logging',
- }
- ),
- learnMoreLink:
- 'https://www.elastic.co/guide/en/fleet/current/elastic-agent-standalone-logging-config.html#elastic-agent-standalone-logging-settings',
- schema: zodStringWithDurationValidation.default('30s'),
- },
];
diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts
index 7f203dd139954..3a56bf248bcf4 100644
--- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts
@@ -719,9 +719,6 @@ describe('getFullAgentPolicy', () => {
mockAgentPolicy({
advanced_settings: {
agent_limits_go_max_procs: 2,
- agent_download_timeout: '60s',
- agent_download_target_directory: '/tmp',
- agent_logging_metrics_period: '10s',
},
});
const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy');
@@ -729,12 +726,7 @@ describe('getFullAgentPolicy', () => {
expect(agentPolicy).toMatchObject({
id: 'agent-policy',
agent: {
- download: {
- timeout: '60s',
- target_directory: '/tmp',
- },
limits: { go_max_procs: 2 },
- logging: { metrics: { period: '10s' } },
},
});
});
From 2b4aecd36d31f932960cbc7adcdb46db5e993bd5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?=
Date: Wed, 10 Apr 2024 12:12:52 +0200
Subject: [PATCH 02/55] =?UTF-8?q?[ObsAiAssistant]=20Add=20=E2=80=9CManage?=
=?UTF-8?q?=20connectors=E2=80=9D=20button=20to=20flyout=20(#179834)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Follow-up to https://github.com/elastic/kibana/pull/179380
This adds a link "Manage connectors" to the flyout where users can
select a connector.
![image](https://github.com/elastic/kibana/assets/209966/8016b1d2-84d7-4372-909e-636677c0de4d)
---
.../components/chat/chat_actions_menu.tsx | 29 +++++++++++++++++--
1 file changed, 27 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_actions_menu.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_actions_menu.tsx
index f06dc126e7667..713a0d2311e3c 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_actions_menu.tsx
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_actions_menu.tsx
@@ -7,7 +7,14 @@
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover, EuiToolTip } from '@elastic/eui';
+import {
+ EuiButtonEmpty,
+ EuiButtonIcon,
+ EuiContextMenu,
+ EuiPanel,
+ EuiPopover,
+ EuiToolTip,
+} from '@elastic/eui';
import { ConnectorSelectorBase } from '@kbn/observability-ai-assistant-plugin/public';
import { useKibana } from '../../hooks/use_kibana';
import { getSettingsHref } from '../../utils/get_settings_href';
@@ -26,11 +33,17 @@ export function ChatActionsMenu({
onCopyConversationClick: () => void;
}) {
const {
- application: { navigateToUrl },
+ application: { navigateToUrl, navigateToApp },
http,
} = useKibana().services;
const [isOpen, setIsOpen] = useState(false);
+ const handleNavigateToConnectors = () => {
+ navigateToApp('management', {
+ path: '/insightsAndAlerting/triggersActionsConnectors/connectors',
+ });
+ };
+
const toggleActionsMenu = () => {
setIsOpen(!isOpen);
};
@@ -139,6 +152,18 @@ export function ChatActionsMenu({
content: (
+
+
+ {i18n.translate(
+ 'xpack.observabilityAiAssistant.settingsPage.goToConnectorsButtonLabel',
+ { defaultMessage: 'Manage connectors' }
+ )}
+
),
},
From 674736d9270f72da5163f51471ef3539101baedf Mon Sep 17 00:00:00 2001
From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com>
Date: Wed, 10 Apr 2024 11:37:34 +0100
Subject: [PATCH 03/55] [Security Solution][Detection Engine] fixes risk score
enrichment for rules with suppression (#180359)
## Summary
Fixes risk score host enrichment for query, threshold, IM rules when
suppression is enabled
Setting Feature Flag `isNewRiskScoreModuleAvailable` to [true by
default](https://elastic.slack.com/archives/C056TQ5J81Y/p1694699742267509)
did not work since enrichment
[function](https://github.com/elastic/kibana/blob/8.13/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.ts#L41)
checks for a flag and if experimentalFeatures property undefined, it
sets `isNewRiskScoreModuleAvailable` to `false`.
Also adds test coverage for new risk score module and for asset
criticality as well
### Flaky tests runner
[FTR ESS
x200](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5634)
[FTR Serverless x200
](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5635)
### 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
---
.../create_indicator_match_alert_type.ts | 3 +-
.../indicator_match/indicator_match.ts | 4 +
.../threat_mapping/create_event_signal.ts | 2 +
.../threat_mapping/create_threat_signal.ts | 2 +
.../threat_mapping/create_threat_signals.ts | 3 +
.../indicator_match/threat_mapping/types.ts | 4 +
...bulk_create_suppressed_alerts_in_memory.ts | 2 +-
.../new_terms/create_new_terms_alert_type.ts | 3 +-
.../group_and_bulk_create.ts | 14 +-
.../rule_types/query/query.ts | 1 +
...bulk_create_suppressed_threshold_alerts.ts | 4 +
.../threshold/create_threshold_alert_type.ts | 3 +-
.../rule_types/threshold/threshold.test.ts | 3 +
.../rule_types/threshold/threshold.ts | 4 +
...bulk_create_suppressed_alerts_in_memory.ts | 4 +-
.../utils/bulk_create_with_suppression.ts | 2 +-
...rch_after_bulk_create_suppressed_alerts.ts | 4 +
.../execution_logic/query.ts | 63 +++++++-
.../threat_match_alert_suppression.ts | 141 ++++++++++++++++++
.../threshold_alert_suppression.ts | 77 ++++++++++
20 files changed, 331 insertions(+), 12 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts
index 75d63f149e68a..3ad9925f7f053 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts
@@ -20,7 +20,7 @@ import type { BuildReasonMessage } from '../utils/reason_formatters';
export const createIndicatorMatchAlertType = (
createOptions: CreateRuleOptions
): SecurityAlertType => {
- const { eventsTelemetry, version, licensing } = createOptions;
+ const { eventsTelemetry, version, licensing, experimentalFeatures } = createOptions;
return {
id: INDICATOR_RULE_TYPE_ID,
name: 'Indicator Match Rule',
@@ -123,6 +123,7 @@ export const createIndicatorMatchAlertType = (
wrapSuppressedHits,
runOpts,
licensing,
+ experimentalFeatures,
});
return { ...result, state };
},
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/indicator_match.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/indicator_match.ts
index 029aee57d8025..7423abe73a51d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/indicator_match.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/indicator_match.ts
@@ -23,6 +23,7 @@ import type { CompleteRule, ThreatRuleParams } from '../../rule_schema';
import { withSecuritySpan } from '../../../../utils/with_security_span';
import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
+import type { ExperimentalFeatures } from '../../../../../common';
export const indicatorMatchExecutor = async ({
inputIndex,
@@ -45,6 +46,7 @@ export const indicatorMatchExecutor = async ({
wrapSuppressedHits,
runOpts,
licensing,
+ experimentalFeatures,
}: {
inputIndex: string[];
runtimeMappings: estypes.MappingRuntimeFields | undefined;
@@ -66,6 +68,7 @@ export const indicatorMatchExecutor = async ({
wrapSuppressedHits: WrapSuppressedHits;
runOpts: RunOpts;
licensing: LicensingPluginSetup;
+ experimentalFeatures: ExperimentalFeatures;
}) => {
const ruleParams = completeRule.ruleParams;
@@ -105,6 +108,7 @@ export const indicatorMatchExecutor = async ({
inputIndexFields,
runOpts,
licensing,
+ experimentalFeatures,
});
});
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts
index acf506b0304c9..8782f34e0afdc 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts
@@ -56,6 +56,7 @@ export const createEventSignal = async ({
completeRule,
sortOrder = 'desc',
isAlertSuppressionActive,
+ experimentalFeatures,
}: CreateEventSignalOptions): Promise => {
const threatFiltersFromEvents = buildThreatMappingFilter({
threatMapping,
@@ -159,6 +160,7 @@ export const createEventSignal = async ({
alertTimestampOverride: runOpts.alertTimestampOverride,
alertWithSuppression: runOpts.alertWithSuppression,
alertSuppression: completeRule.ruleParams.alertSuppression,
+ experimentalFeatures,
});
} else {
createResult = await searchAfterAndBulkCreate(searchAfterBulkCreateParams);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts
index 7740bed7777bb..318f8ddfbb759 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts
@@ -54,6 +54,7 @@ export const createThreatSignal = async ({
threatIndexFields,
sortOrder = 'desc',
isAlertSuppressionActive,
+ experimentalFeatures,
}: CreateThreatSignalOptions): Promise => {
const threatFilter = buildThreatMappingFilter({
threatMapping,
@@ -132,6 +133,7 @@ export const createThreatSignal = async ({
alertTimestampOverride: runOpts.alertTimestampOverride,
alertWithSuppression: runOpts.alertWithSuppression,
alertSuppression: completeRule.ruleParams.alertSuppression,
+ experimentalFeatures,
});
} else {
result = await searchAfterAndBulkCreate(searchAfterBulkCreateParams);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts
index bb01cbe9a5b48..894277d5ad17f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts
@@ -68,6 +68,7 @@ export const createThreatSignals = async ({
unprocessedExceptions,
inputIndexFields,
licensing,
+ experimentalFeatures,
}: CreateThreatSignalsOptions): Promise => {
const threatMatchedFields = getMatchedFields(threatMapping);
const allowedFieldsForTermsQuery = await getAllowedFieldsForTermQuery({
@@ -296,6 +297,7 @@ export const createThreatSignals = async ({
runOpts,
sortOrder,
isAlertSuppressionActive,
+ experimentalFeatures,
}),
});
} else {
@@ -361,6 +363,7 @@ export const createThreatSignals = async ({
runOpts,
sortOrder,
isAlertSuppressionActive,
+ experimentalFeatures,
}),
});
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts
index d8a6a97bbc644..e1072f873917a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts
@@ -41,6 +41,7 @@ import type {
} from '../../types';
import type { CompleteRule, ThreatRuleParams } from '../../../rule_schema';
import type { IRuleExecutionLogForExecutors } from '../../../rule_monitoring';
+import type { ExperimentalFeatures } from '../../../../../../common';
export type SortOrderOrUndefined = 'asc' | 'desc' | undefined;
@@ -79,6 +80,7 @@ export interface CreateThreatSignalsOptions {
inputIndexFields: DataViewFieldBase[];
runOpts: RunOpts;
licensing: LicensingPluginSetup;
+ experimentalFeatures: ExperimentalFeatures;
}
export interface CreateThreatSignalOptions {
@@ -122,6 +124,7 @@ export interface CreateThreatSignalOptions {
runOpts: RunOpts;
sortOrder?: SortOrderOrUndefined;
isAlertSuppressionActive: boolean;
+ experimentalFeatures: ExperimentalFeatures;
}
export interface CreateEventSignalOptions {
@@ -166,6 +169,7 @@ export interface CreateEventSignalOptions {
runOpts: RunOpts;
sortOrder?: SortOrderOrUndefined;
isAlertSuppressionActive: boolean;
+ experimentalFeatures: ExperimentalFeatures;
}
type EntryKey = 'field' | 'value';
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/bulk_create_suppressed_alerts_in_memory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/bulk_create_suppressed_alerts_in_memory.ts
index 8d39fcccee62e..2fffa07f8d684 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/bulk_create_suppressed_alerts_in_memory.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/bulk_create_suppressed_alerts_in_memory.ts
@@ -54,7 +54,7 @@ export interface BulkCreateSuppressedAlertsParams
>;
eventsAndTerms: EventsAndTerms[];
toReturn: SearchAfterAndBulkCreateReturnType;
- experimentalFeatures: ExperimentalFeatures | undefined;
+ experimentalFeatures: ExperimentalFeatures;
}
/**
* wraps, bulk create and suppress alerts in memory, also takes care of missing fields logic.
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts
index 42ffec0b6236c..f75b4e229f3d8 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts
@@ -48,7 +48,7 @@ import type { GenericBulkCreateResponse } from '../utils/bulk_create_with_suppre
export const createNewTermsAlertType = (
createOptions: CreateRuleOptions
): SecurityAlertType => {
- const { logger, licensing } = createOptions;
+ const { logger, licensing, experimentalFeatures } = createOptions;
return {
id: NEW_TERMS_RULE_TYPE_ID,
name: 'New Terms Rule',
@@ -110,7 +110,6 @@ export const createNewTermsAlertType = (
alertTimestampOverride,
publicBaseUrl,
inputIndexFields,
- experimentalFeatures,
alertWithSuppression,
},
services,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/alert_suppression/group_and_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/alert_suppression/group_and_bulk_create.ts
index ced44553192e6..b1d897250ca4e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/alert_suppression/group_and_bulk_create.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/alert_suppression/group_and_bulk_create.ts
@@ -30,6 +30,8 @@ import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../../com
import { bulkCreateUnsuppressedAlerts } from './bulk_create_unsuppressed_alerts';
import type { ITelemetryEventsSender } from '../../../../telemetry/sender';
import { DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY } from '../../../../../../common/detection_engine/constants';
+import type { ExperimentalFeatures } from '../../../../../../common';
+import { createEnrichEventsFunction } from '../../utils/enrichments';
export interface BucketHistory {
key: Record;
@@ -45,6 +47,7 @@ export interface GroupAndBulkCreateParams {
bucketHistory?: BucketHistory[];
groupByFields: string[];
eventsTelemetry: ITelemetryEventsSender | undefined;
+ experimentalFeatures: ExperimentalFeatures;
}
export interface GroupAndBulkCreateReturnType extends SearchAfterAndBulkCreateReturnType {
@@ -123,6 +126,7 @@ export const groupAndBulkCreate = async ({
bucketHistory,
groupByFields,
eventsTelemetry,
+ experimentalFeatures,
}: GroupAndBulkCreateParams): Promise => {
return withSecuritySpan('groupAndBulkCreate', async () => {
const tuple = runOpts.tuple;
@@ -269,11 +273,19 @@ export const groupAndBulkCreate = async ({
services,
suppressionWindow,
alertTimestampOverride: runOpts.alertTimestampOverride,
+ experimentalFeatures,
});
addToSearchAfterReturn({ current: toReturn, next: bulkCreateResult });
runOpts.ruleExecutionLogger.debug(`created ${bulkCreateResult.createdItemsCount} signals`);
} else {
- const bulkCreateResult = await runOpts.bulkCreate(wrappedAlerts);
+ const bulkCreateResult = await runOpts.bulkCreate(
+ wrappedAlerts,
+ undefined,
+ createEnrichEventsFunction({
+ services,
+ logger: runOpts.ruleExecutionLogger,
+ })
+ );
addToSearchAfterReturn({ current: toReturn, next: bulkCreateResult });
runOpts.ruleExecutionLogger.debug(`created ${bulkCreateResult.createdItemsCount} signals`);
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts
index cd6139674ecbf..b3de5a39d829f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts
@@ -75,6 +75,7 @@ export const queryExecutor = async ({
bucketHistory,
groupByFields: ruleParams.alertSuppression.groupBy,
eventsTelemetry,
+ experimentalFeatures,
})
: {
...(await searchAfterAndBulkCreate({
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/bulk_create_suppressed_threshold_alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/bulk_create_suppressed_threshold_alerts.ts
index b2ec6fbc909d3..b5e288909b412 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/bulk_create_suppressed_threshold_alerts.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/bulk_create_suppressed_threshold_alerts.ts
@@ -23,6 +23,7 @@ import { bulkCreateWithSuppression } from '../utils/bulk_create_with_suppression
import type { GenericBulkCreateResponse } from '../utils/bulk_create_with_suppression';
import { wrapSuppressedThresholdALerts } from './wrap_suppressed_threshold_alerts';
import { transformBulkCreatedItemsToHits } from './utils';
+import type { ExperimentalFeatures } from '../../../../../common';
interface BulkCreateSuppressedThresholdAlertsParams {
buckets: ThresholdBucket[];
@@ -35,6 +36,7 @@ interface BulkCreateSuppressedThresholdAlertsParams {
ruleExecutionLogger: IRuleExecutionLogForExecutors;
spaceId: string;
runOpts: RunOpts;
+ experimentalFeatures: ExperimentalFeatures;
}
/**
@@ -53,6 +55,7 @@ export const bulkCreateSuppressedThresholdAlerts = async ({
ruleExecutionLogger,
spaceId,
runOpts,
+ experimentalFeatures,
}: BulkCreateSuppressedThresholdAlertsParams): Promise<{
bulkCreateResult: GenericBulkCreateResponse;
unsuppressedAlerts: Array>;
@@ -90,6 +93,7 @@ export const bulkCreateSuppressedThresholdAlerts = async ({
services,
suppressionWindow,
alertTimestampOverride: runOpts.alertTimestampOverride,
+ experimentalFeatures,
});
return {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts
index 11ed8e74e0ca5..84f3a52af95e2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts
@@ -19,7 +19,7 @@ import { validateIndexPatterns } from '../utils';
export const createThresholdAlertType = (
createOptions: CreateRuleOptions
): SecurityAlertType => {
- const { version, licensing } = createOptions;
+ const { version, licensing, experimentalFeatures } = createOptions;
return {
id: THRESHOLD_RULE_TYPE_ID,
name: 'Threshold Rule',
@@ -103,6 +103,7 @@ export const createThresholdAlertType = (
spaceId,
runOpts: execOptions.runOpts,
licensing,
+ experimentalFeatures,
});
return result;
},
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.test.ts
index 342776d0fcc1a..50a315a58ef4d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.test.ts
@@ -20,6 +20,7 @@ import { createRuleDataClientMock } from '@kbn/rule-registry-plugin/server/rule_
import { TIMESTAMP } from '@kbn/rule-data-utils';
import { ruleExecutionLogMock } from '../../rule_monitoring/mocks';
import type { RunOpts } from '../types';
+import type { ExperimentalFeatures } from '../../../../../common';
describe('threshold_executor', () => {
let alertServices: RuleExecutorServicesMock;
@@ -110,6 +111,7 @@ describe('threshold_executor', () => {
spaceId: 'default',
runOpts: {} as RunOpts,
licensing,
+ experimentalFeatures: {} as ExperimentalFeatures,
});
expect(response.state).toEqual({
initialized: true,
@@ -175,6 +177,7 @@ describe('threshold_executor', () => {
spaceId: 'default',
runOpts: {} as RunOpts,
licensing,
+ experimentalFeatures: {} as ExperimentalFeatures,
});
expect(result.warningMessages).toEqual([
`The following exceptions won't be applied to rule execution: ${
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts
index 4ade5ff1e2f09..ee0e80f03df15 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts
@@ -44,6 +44,7 @@ import { withSecuritySpan } from '../../../../utils/with_security_span';
import { buildThresholdSignalHistory } from './build_signal_history';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
import { getSignalHistory, transformBulkCreatedItemsToHits } from './utils';
+import type { ExperimentalFeatures } from '../../../../../common';
export const thresholdExecutor = async ({
inputIndex,
@@ -67,6 +68,7 @@ export const thresholdExecutor = async ({
spaceId,
runOpts,
licensing,
+ experimentalFeatures,
}: {
inputIndex: string[];
runtimeMappings: estypes.MappingRuntimeFields | undefined;
@@ -89,6 +91,7 @@ export const thresholdExecutor = async ({
spaceId: string;
runOpts: RunOpts;
licensing: LicensingPluginSetup;
+ experimentalFeatures: ExperimentalFeatures;
}): Promise => {
const result = createSearchAfterReturnType();
const ruleParams = completeRule.ruleParams;
@@ -167,6 +170,7 @@ export const thresholdExecutor = async ({
ruleExecutionLogger,
spaceId,
runOpts,
+ experimentalFeatures,
});
const createResult = suppressedResults.bulkCreateResult;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_suppressed_alerts_in_memory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_suppressed_alerts_in_memory.ts
index 94d7003c4eb13..85bfc76e964e2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_suppressed_alerts_in_memory.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_suppressed_alerts_in_memory.ts
@@ -50,7 +50,7 @@ export interface BulkCreateSuppressedAlertsParams
> {
enrichedEvents: SignalSourceHit[];
toReturn: SearchAfterAndBulkCreateReturnType;
- experimentalFeatures?: ExperimentalFeatures;
+ experimentalFeatures: ExperimentalFeatures;
}
/**
* wraps, bulk create and suppress alerts in memory, also takes care of missing fields logic.
@@ -119,7 +119,7 @@ export interface ExecuteBulkCreateAlertsParams>;
suppressibleWrappedDocs: Array>;
toReturn: SearchAfterAndBulkCreateReturnType;
- experimentalFeatures?: ExperimentalFeatures;
+ experimentalFeatures: ExperimentalFeatures;
}
/**
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_with_suppression.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_with_suppression.ts
index 35d55ec7f39d2..75aa46d039277 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_with_suppression.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/bulk_create_with_suppression.ts
@@ -55,7 +55,7 @@ export const bulkCreateWithSuppression = async <
alertTimestampOverride: Date | undefined;
isSuppressionPerRuleExecution?: boolean;
maxAlerts?: number;
- experimentalFeatures?: ExperimentalFeatures;
+ experimentalFeatures: ExperimentalFeatures;
}): Promise> => {
if (wrappedDocs.length === 0) {
return {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create_suppressed_alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create_suppressed_alerts.ts
index 0a89dec4bd251..1e63fec53597b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create_suppressed_alerts.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/search_after_bulk_create_suppressed_alerts.ts
@@ -14,12 +14,14 @@ import type {
WrapSuppressedHits,
} from '../types';
import type { AlertSuppressionCamel } from '../../../../../common/api/detection_engine/model/rule_schema';
+import type { ExperimentalFeatures } from '../../../../../common';
interface SearchAfterAndBulkCreateSuppressedAlertsParams extends SearchAfterAndBulkCreateParams {
wrapSuppressedHits: WrapSuppressedHits;
alertTimestampOverride: Date | undefined;
alertWithSuppression: SuppressedAlertService;
alertSuppression?: AlertSuppressionCamel;
+ experimentalFeatures: ExperimentalFeatures;
}
import type { SearchAfterAndBulkCreateFactoryParams } from './search_after_bulk_create_factory';
@@ -44,6 +46,7 @@ export const searchAfterAndBulkCreateSuppressedAlerts = async (
wrapSuppressedHits,
alertWithSuppression,
alertTimestampOverride,
+ experimentalFeatures,
} = params;
const bulkCreateExecutor: SearchAfterAndBulkCreateFactoryParams['bulkCreateExecutor'] = async ({
@@ -63,6 +66,7 @@ export const searchAfterAndBulkCreateSuppressedAlerts = async (
alertTimestampOverride,
enrichedEvents,
toReturn,
+ experimentalFeatures,
});
};
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/query.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/query.ts
index 4e09b5868204f..dd9c0dc624035 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/query.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/query.ts
@@ -98,8 +98,7 @@ export default ({ getService }: FtrProviderContext) => {
const dataPathBuilder = new EsArchivePathBuilder(isServerless);
const auditbeatPath = dataPathBuilder.getPath('auditbeat/hosts');
- // FLAKY: https://github.com/elastic/kibana/issues/177101
- describe.skip('@ess @serverless Query type rules', () => {
+ describe('@ess @serverless Query type rules', () => {
before(async () => {
await esArchiver.load(auditbeatPath);
await esArchiver.load('x-pack/test/functional/es_archives/security_solution/alerts/8.8.0', {
@@ -203,7 +202,8 @@ export default ({ getService }: FtrProviderContext) => {
});
});
- it('should query and get back expected alert structure when it is a alert on a alert', async () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/177101
+ it.skip('should query and get back expected alert structure when it is a alert on a alert', async () => {
const alertId = 'eabbdefc23da981f2b74ab58b82622a97bb9878caa11bc914e2adfacc94780f1';
const rule: QueryRuleCreateProps = {
...getRuleForAlertTesting([`.alerts-security.alerts-default*`]),
@@ -278,6 +278,45 @@ export default ({ getService }: FtrProviderContext) => {
};
const { previewId } = await previewRule({ supertest, rule });
const previewAlerts = await getPreviewAlerts({ es, previewId });
+
+ expect(previewAlerts[0]?._source?.host?.risk?.calculated_level).to.eql('Critical');
+ expect(previewAlerts[0]?._source?.host?.risk?.calculated_score_norm).to.eql(96);
+ expect(previewAlerts[0]?._source?.user?.risk?.calculated_level).to.eql('Low');
+ expect(previewAlerts[0]?._source?.user?.risk?.calculated_score_norm).to.eql(11);
+ });
+
+ it('should have host and user risk score fields when suppression enabled on interval', async () => {
+ const rule: QueryRuleCreateProps = {
+ ...getRuleForAlertTesting(['auditbeat-*']),
+ query: `_id:${ID}`,
+ alert_suppression: {
+ group_by: ['host.name'],
+ duration: {
+ value: 300,
+ unit: 'm',
+ },
+ },
+ };
+ const { previewId } = await previewRule({ supertest, rule });
+ const previewAlerts = await getPreviewAlerts({ es, previewId });
+
+ expect(previewAlerts[0]?._source?.host?.risk?.calculated_level).to.eql('Critical');
+ expect(previewAlerts[0]?._source?.host?.risk?.calculated_score_norm).to.eql(96);
+ expect(previewAlerts[0]?._source?.user?.risk?.calculated_level).to.eql('Low');
+ expect(previewAlerts[0]?._source?.user?.risk?.calculated_score_norm).to.eql(11);
+ });
+
+ it('should have host and user risk score fields when suppression enabled on rule execution only', async () => {
+ const rule: QueryRuleCreateProps = {
+ ...getRuleForAlertTesting(['auditbeat-*']),
+ query: `_id:${ID}`,
+ alert_suppression: {
+ group_by: ['host.name'],
+ },
+ };
+ const { previewId } = await previewRule({ supertest, rule });
+ const previewAlerts = await getPreviewAlerts({ es, previewId });
+
expect(previewAlerts[0]?._source?.host?.risk?.calculated_level).to.eql('Critical');
expect(previewAlerts[0]?._source?.host?.risk?.calculated_score_norm).to.eql(96);
expect(previewAlerts[0]?._source?.user?.risk?.calculated_level).to.eql('Low');
@@ -307,6 +346,24 @@ export default ({ getService }: FtrProviderContext) => {
expect(previewAlerts[0]?._source?.['host.asset.criticality']).to.eql('high_impact');
expect(previewAlerts[0]?._source?.['user.asset.criticality']).to.eql('extreme_impact');
});
+
+ it('should be enriched alert with criticality_level when suppression enabled', async () => {
+ const rule: QueryRuleCreateProps = {
+ ...getRuleForAlertTesting(['auditbeat-*']),
+ query: `_id:${ID}`,
+ alert_suppression: {
+ group_by: ['host.name'],
+ duration: {
+ value: 300,
+ unit: 'm',
+ },
+ },
+ };
+ const { previewId } = await previewRule({ supertest, rule });
+ const previewAlerts = await getPreviewAlerts({ es, previewId });
+ expect(previewAlerts[0]?._source?.['host.asset.criticality']).to.eql('high_impact');
+ expect(previewAlerts[0]?._source?.['user.asset.criticality']).to.eql('extreme_impact');
+ });
});
/**
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match_alert_suppression.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match_alert_suppression.ts
index dec39659ae256..84e6ce0469367 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match_alert_suppression.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match_alert_suppression.ts
@@ -20,6 +20,7 @@ import {
import { getSuppressionMaxSignalsWarning as getSuppressionMaxAlertsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
import { DETECTION_ENGINE_SIGNALS_STATUS_URL as DETECTION_ENGINE_ALERTS_STATUS_URL } from '@kbn/security-solution-plugin/common/constants';
+import { ENABLE_ASSET_CRITICALITY_SETTING } from '@kbn/security-solution-plugin/common/constants';
import { ThreatMatchRuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { RuleExecutionStatusEnum } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
@@ -42,6 +43,7 @@ export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const es = getService('es');
const log = getService('log');
+ const kibanaServer = getService('kibanaServer');
const {
indexListOfDocuments: indexListOfSourceDocuments,
@@ -2388,6 +2390,145 @@ export default ({ getService }: FtrProviderContext) => {
});
});
});
+
+ describe('alerts should be enriched', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/entity/risks');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/entity/risks');
+ });
+
+ it('should be enriched with host risk score', async () => {
+ const id = uuidv4();
+ const timestamp = '2020-10-28T06:45:00.000Z';
+ const laterTimestamp = '2020-10-28T06:50:00.000Z';
+ const doc1 = {
+ id,
+ '@timestamp': timestamp,
+ host: { name: 'zeek-sensor-amsterdam' },
+ user: { name: 'root' },
+ };
+ const doc1WithLaterTimestamp = {
+ ...doc1,
+ '@timestamp': laterTimestamp,
+ };
+
+ await eventsFiller({ id, count: eventsCount, timestamp: [timestamp] });
+ await threatsFiller({ id, count: threatsCount, timestamp });
+
+ await indexListOfSourceDocuments([doc1, doc1WithLaterTimestamp, doc1]);
+
+ await addThreatDocuments({
+ id,
+ timestamp,
+ fields: {
+ host: {
+ name: 'zeek-sensor-amsterdam',
+ },
+ },
+ count: 1,
+ });
+
+ const rule: ThreatMatchRuleCreateProps = {
+ ...indicatorMatchRule(id),
+ alert_suppression: {
+ group_by: ['host.name'],
+ missing_fields_strategy: 'suppress',
+ },
+ from: 'now-35m',
+ interval: '30m',
+ };
+
+ const { previewId } = await previewRule({
+ supertest,
+ rule,
+ timeframeEnd: new Date('2020-10-28T07:00:00.000Z'),
+ invocationCount: 1,
+ });
+ const previewAlerts = await getPreviewAlerts({
+ es,
+ previewId,
+ sort: [ALERT_ORIGINAL_TIME],
+ });
+
+ expect(previewAlerts[0]?._source?.host?.risk?.calculated_level).toEqual('Critical');
+ expect(previewAlerts[0]?._source?.host?.risk?.calculated_score_norm).toEqual(70);
+ expect(previewAlerts[0]?._source?.user?.risk?.calculated_level).toEqual('Low');
+ expect(previewAlerts[0]?._source?.user?.risk?.calculated_score_norm).toEqual(11);
+ });
+ });
+
+ describe('with asset criticality', async () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/asset_criticality');
+ await kibanaServer.uiSettings.update({
+ [ENABLE_ASSET_CRITICALITY_SETTING]: true,
+ });
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/asset_criticality');
+ });
+
+ it('should be enriched alert with criticality_level', async () => {
+ const id = uuidv4();
+ const timestamp = '2020-10-28T06:45:00.000Z';
+ const laterTimestamp = '2020-10-28T06:50:00.000Z';
+ const doc1 = {
+ id,
+ '@timestamp': timestamp,
+ host: { name: 'zeek-sensor-amsterdam' },
+ user: { name: 'root' },
+ };
+ const doc1WithLaterTimestamp = {
+ ...doc1,
+ '@timestamp': laterTimestamp,
+ };
+
+ await eventsFiller({ id, count: eventsCount, timestamp: [timestamp] });
+ await threatsFiller({ id, count: threatsCount, timestamp });
+
+ await indexListOfSourceDocuments([doc1, doc1WithLaterTimestamp, doc1]);
+
+ await addThreatDocuments({
+ id,
+ timestamp,
+ fields: {
+ host: {
+ name: 'zeek-sensor-amsterdam',
+ },
+ },
+ count: 1,
+ });
+
+ const rule: ThreatMatchRuleCreateProps = {
+ ...indicatorMatchRule(id),
+ alert_suppression: {
+ group_by: ['host.name'],
+ missing_fields_strategy: 'suppress',
+ },
+ from: 'now-35m',
+ interval: '30m',
+ };
+
+ const { previewId } = await previewRule({
+ supertest,
+ rule,
+ timeframeEnd: new Date('2020-10-28T07:00:00.000Z'),
+ invocationCount: 1,
+ });
+ const previewAlerts = await getPreviewAlerts({
+ es,
+ previewId,
+ sort: [ALERT_ORIGINAL_TIME],
+ });
+
+ expect(previewAlerts[0]?._source?.['host.asset.criticality']).toEqual('low_impact');
+ expect(previewAlerts[0]?._source?.['user.asset.criticality']).toEqual('extreme_impact');
+ });
+ });
});
});
});
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threshold_alert_suppression.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threshold_alert_suppression.ts
index 9a5763e95c51c..11b38b71599df 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threshold_alert_suppression.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threshold_alert_suppression.ts
@@ -21,6 +21,7 @@ import { DETECTION_ENGINE_SIGNALS_STATUS_URL as DETECTION_ENGINE_ALERTS_STATUS_U
import { ThresholdRuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { RuleExecutionStatusEnum } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
+import { ENABLE_ASSET_CRITICALITY_SETTING } from '@kbn/security-solution-plugin/common/constants';
import { ALERT_ORIGINAL_TIME } from '@kbn/security-solution-plugin/common/field_maps/field_names';
import { createRule } from '../../../../../../../common/utils/security_solution';
@@ -34,12 +35,19 @@ import {
dataGeneratorFactory,
} from '../../../../utils';
import { FtrProviderContext } from '../../../../../../ftr_provider_context';
+import { EsArchivePathBuilder } from '../../../../../../es_archive_path_builder';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const es = getService('es');
const log = getService('log');
+ const kibanaServer = getService('kibanaServer');
+ // TODO: add a new service for loading archiver files similar to "getService('es')"
+ const config = getService('config');
+ const isServerless = config.get('serverless');
+ const dataPathBuilder = new EsArchivePathBuilder(isServerless);
+ const path = dataPathBuilder.getPath('auditbeat/hosts');
describe('@ess @serverless Threshold type rules, alert suppression', () => {
const { indexListOfDocuments, indexGeneratedDocuments } = dataGeneratorFactory({
@@ -49,10 +57,12 @@ export default ({ getService }: FtrProviderContext) => {
});
before(async () => {
+ await esArchiver.load(path);
await esArchiver.load('x-pack/test/functional/es_archives/security_solution/ecs_compliant');
});
after(async () => {
+ await esArchiver.unload(path);
await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/ecs_compliant');
});
@@ -854,5 +864,72 @@ export default ({ getService }: FtrProviderContext) => {
})
);
});
+
+ describe('with host risk index', async () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/entity/risks');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/entity/risks');
+ });
+
+ it('should be enriched with host risk score', async () => {
+ const rule: ThresholdRuleCreateProps = {
+ ...getThresholdRuleForAlertTesting(['auditbeat-*']),
+ threshold: {
+ field: 'host.name',
+ value: 100,
+ },
+ alert_suppression: {
+ duration: {
+ value: 300,
+ unit: 'm',
+ },
+ },
+ };
+ const { previewId } = await previewRule({ supertest, rule });
+ const previewAlerts = await getPreviewAlerts({ es, previewId, sort: ['host.name'] });
+
+ expect(previewAlerts[0]?._source?.host?.risk?.calculated_level).toEqual('Low');
+ expect(previewAlerts[0]?._source?.host?.risk?.calculated_score_norm).toEqual(20);
+ expect(previewAlerts[1]?._source?.host?.risk?.calculated_level).toEqual('Critical');
+ expect(previewAlerts[1]?._source?.host?.risk?.calculated_score_norm).toEqual(96);
+ });
+ });
+
+ describe('with asset criticality', async () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/asset_criticality');
+ await kibanaServer.uiSettings.update({
+ [ENABLE_ASSET_CRITICALITY_SETTING]: true,
+ });
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/asset_criticality');
+ });
+
+ it('should be enriched alert with criticality_level', async () => {
+ const rule: ThresholdRuleCreateProps = {
+ ...getThresholdRuleForAlertTesting(['auditbeat-*']),
+ threshold: {
+ field: 'host.name',
+ value: 100,
+ },
+ alert_suppression: {
+ duration: {
+ value: 300,
+ unit: 'm',
+ },
+ },
+ };
+ const { previewId } = await previewRule({ supertest, rule });
+ const previewAlerts = await getPreviewAlerts({ es, previewId, sort: ['host.name'] });
+ const fullAlert = previewAlerts[0]?._source;
+
+ expect(fullAlert?.['host.asset.criticality']).toEqual('high_impact');
+ });
+ });
});
};
From d3fe5434b34d45cad02b599503a90a132c0f0ae3 Mon Sep 17 00:00:00 2001
From: jennypavlova
Date: Wed, 10 Apr 2024 12:45:25 +0200
Subject: [PATCH 04/55] [Infra] Implement dashboard tab UI in host details
(#178518)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes #176070
Closes #178319
Closes [#175447](https://github.com/elastic/kibana/issues/175447)
## Summary
This PR adds a dashboard tab to the asset details view. This is the
first version of the tab and it looks similar to the APM solution in
service overview.
⚠️ After [this PR](https://github.com/elastic/kibana/pull/179576) was
merged the structure of the API changed and the saved object is now one
per linked dashboard (not one per asset type as before). Those changes
will give us more flexibility in the future and the endpoints allow us
to edit/link/unlink a dashboard easier than before (and we don't have to
get and iterate through all dashboards when updating/deleting)
The new structure of the saved object is now :
```javascript
"properties": {
"assetType": {
"type": "keyword"
},
"dashboardSavedObjectId": {
"type": "keyword"
},
"dashboardFilterAssetIdEnabled": {
"type": "boolean"
}
}
```
This initial implementation will show the dashboard tab **ONLY** if the
feature flag (`enableInfrastructureAssetCustomDashboards`) is enabled
(this will change)
## Updates:
- #178319 New splash screen
- The new API endpoints added in [this
PR](https://github.com/elastic/kibana/pull/179576) are now used
- Fix switching dashboards not refreshing the content and not applying
filters issue
## Next steps
- [ ] [[Infra] Dashboard locator |
kibana#178520](https://github.com/elastic/kibana/issues/178520)
- [ ] [[Infra] Dashboard feature activation |
kibana#175542](https://github.com/elastic/kibana/issues/175542)
## Testing
- Generate some hosts with metrics:
- `node scripts/synthtrace --clean --scenarioOpts.numServices=5
infra_hosts_with_apm_hosts.ts`
- or use metricbeat/remote cluster
- Enable the `enableInfrastructureAssetCustomDashboards` feature flag
- Go to Hosts view flyout / Go to Asset details page
- Link a dashboard
- Edit the linked dashboard (enable/disable filter by hostname)
https://github.com/elastic/kibana/assets/14139027/ad6b87aa-e2de-42fa-9565-4bfe32ffd146
- Unlink a dashboard: In case of unlinking:
- single dashboard -> empty state
- multiple dashboards -> other dashboard
https://github.com/elastic/kibana/assets/14139027/4f39f3aa-b7fa-407d-8991-79d19d3ee076
- Navigation between Hosts view flyout / Asset details page (Click `Open
as page`) and persisting the state:
https://github.com/elastic/kibana/assets/14139027/98756cb0-7675-4bc0-9e14-0fb8d95cce30
- Link custom dashboard (create a dashboard and link it after)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../actions/save_dashboard_modal.tsx | 2 +-
.../infra/common/custom_dashboards.ts | 4 +
.../common/http_api/custom_dashboards_api.ts | 3 +-
.../observability_solution/infra/kibana.jsonc | 1 +
.../asset_details_tabs.tsx | 6 +
.../asset_details/content/content.tsx | 15 +-
.../hooks/use_asset_details_url_state.ts | 2 +
.../hooks/use_custom_dashboards.ts | 211 ++++++++++++++
.../hooks/use_dashboards_fetcher.ts | 69 +++++
.../hooks/use_fetch_custom_dashboards.ts | 57 ++++
.../asset_details/hooks/use_page_header.tsx | 8 +-
.../dashboards/actions/edit_dashboard.tsx | 52 ++++
.../actions/goto_dashboard_link.tsx | 40 +++
.../tabs/dashboards/actions/index.ts | 11 +
.../dashboards/actions/link_dashboard.tsx | 63 ++++
.../actions/save_dashboard_modal.tsx | 275 ++++++++++++++++++
.../dashboards/actions/unlink_dashboard.tsx | 129 ++++++++
.../tabs/dashboards/context_menu.tsx | 49 ++++
.../tabs/dashboards/dashboard_selector.tsx | 88 ++++++
.../tabs/dashboards/dashboards.tsx | 209 +++++++++++++
.../tabs/dashboards/empty_dashboards.tsx | 65 +++++
.../components/asset_details/tabs/index.ts | 1 +
.../public/components/asset_details/types.ts | 1 +
.../public/hooks/use_saved_views_notifier.ts | 4 +-
.../infra/public/types.ts | 2 +
.../infra/public/utils/filters/build.test.ts | 32 ++
.../infra/public/utils/filters/build.ts | 13 +-
.../infra/tsconfig.json | 2 +
.../test/functional/apps/infra/hosts_view.ts | 21 +-
.../functional/page_objects/asset_details.ts | 9 +
30 files changed, 1435 insertions(+), 9 deletions(-)
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_custom_dashboards.ts
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_dashboards_fetcher.ts
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_fetch_custom_dashboards.ts
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/edit_dashboard.tsx
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/goto_dashboard_link.tsx
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/index.ts
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/link_dashboard.tsx
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/save_dashboard_modal.tsx
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/unlink_dashboard.tsx
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/context_menu.tsx
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboard_selector.tsx
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboards.tsx
create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/empty_dashboards.tsx
create mode 100644 x-pack/plugins/observability_solution/infra/public/utils/filters/build.test.ts
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx
index 406a1b3e43190..f95451383ad0d 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx
@@ -282,7 +282,7 @@ function getEditSuccessToastLabels(dashboardName: string) {
}
),
text: i18n.translate('xpack.apm.serviceDashboards.editSuccess.toast.text', {
- defaultMessage: 'Your dashboard link have been updated',
+ defaultMessage: 'Your dashboard link has been updated',
}),
};
}
diff --git a/x-pack/plugins/observability_solution/infra/common/custom_dashboards.ts b/x-pack/plugins/observability_solution/infra/common/custom_dashboards.ts
index 41bab533cf7d4..0861934653fe9 100644
--- a/x-pack/plugins/observability_solution/infra/common/custom_dashboards.ts
+++ b/x-pack/plugins/observability_solution/infra/common/custom_dashboards.ts
@@ -18,3 +18,7 @@ export interface InfraCustomDashboard {
export interface InfraSavedCustomDashboard extends InfraCustomDashboard {
id: string;
}
+
+export interface DashboardItemWithTitle extends InfraSavedCustomDashboard {
+ title: string;
+}
diff --git a/x-pack/plugins/observability_solution/infra/common/http_api/custom_dashboards_api.ts b/x-pack/plugins/observability_solution/infra/common/http_api/custom_dashboards_api.ts
index f89f66ca86e37..c302a2a617def 100644
--- a/x-pack/plugins/observability_solution/infra/common/http_api/custom_dashboards_api.ts
+++ b/x-pack/plugins/observability_solution/infra/common/http_api/custom_dashboards_api.ts
@@ -21,7 +21,7 @@ const SavedObjectIdRT = rt.type({
id: rt.string,
});
-const InfraCustomDashboardRT = rt.intersection([AssetTypeRT, PayloadRT, SavedObjectIdRT]);
+export const InfraCustomDashboardRT = rt.intersection([AssetTypeRT, PayloadRT, SavedObjectIdRT]);
/**
GET endpoint
@@ -59,3 +59,4 @@ export const InfraDeleteCustomDashboardsRequestParamsRT = rt.intersection([
AssetTypeRT,
SavedObjectIdRT,
]);
+export const InfraDeleteCustomDashboardsResponseBodyRT = rt.string;
diff --git a/x-pack/plugins/observability_solution/infra/kibana.jsonc b/x-pack/plugins/observability_solution/infra/kibana.jsonc
index 22035e8d8fc23..973acf3e75d55 100644
--- a/x-pack/plugins/observability_solution/infra/kibana.jsonc
+++ b/x-pack/plugins/observability_solution/infra/kibana.jsonc
@@ -16,6 +16,7 @@
"data",
"dataViews",
"dataViewEditor",
+ "dashboard",
"discover",
"embeddable",
"features",
diff --git a/x-pack/plugins/observability_solution/infra/public/common/asset_details_config/asset_details_tabs.tsx b/x-pack/plugins/observability_solution/infra/public/common/asset_details_config/asset_details_tabs.tsx
index 312ed7eb04a98..913622a80f9ab 100644
--- a/x-pack/plugins/observability_solution/infra/public/common/asset_details_config/asset_details_tabs.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/common/asset_details_config/asset_details_tabs.tsx
@@ -51,4 +51,10 @@ export const commonFlyoutTabs: Tab[] = [
defaultMessage: 'Osquery',
}),
},
+ {
+ id: ContentTabIds.DASHBOARDS,
+ name: i18n.translate('xpack.infra.infra.nodeDetails.tabs.dashboards', {
+ defaultMessage: 'Dashboards',
+ }),
+ },
];
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx
index 659304402060a..6524f34252fa6 100644
--- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx
@@ -9,7 +9,16 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { DatePicker } from '../date_picker/date_picker';
import { useTabSwitcherContext } from '../hooks/use_tab_switcher';
-import { Anomalies, Logs, Metadata, Osquery, Overview, Processes, Profiling } from '../tabs';
+import {
+ Anomalies,
+ Dashboards,
+ Logs,
+ Metadata,
+ Osquery,
+ Overview,
+ Processes,
+ Profiling,
+} from '../tabs';
import { ContentTabIds } from '../types';
export const Content = () => {
@@ -23,6 +32,7 @@ export const Content = () => {
ContentTabIds.METADATA,
ContentTabIds.PROCESSES,
ContentTabIds.ANOMALIES,
+ ContentTabIds.DASHBOARDS,
]}
/>
@@ -48,6 +58,9 @@ export const Content = () => {
+
+
+
);
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts
index b7411bb188e20..fc617292ad4e9 100644
--- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts
@@ -59,6 +59,7 @@ const TabIdRT = rt.union([
rt.literal(ContentTabIds.LOGS),
rt.literal(ContentTabIds.ANOMALIES),
rt.literal(ContentTabIds.OSQUERY),
+ rt.literal(ContentTabIds.DASHBOARDS),
]);
const AlertStatusRT = rt.union([
@@ -84,6 +85,7 @@ const AssetDetailsUrlStateRT = rt.partial({
logsSearch: rt.string,
profilingSearch: rt.string,
alertStatus: AlertStatusRT,
+ dashboardId: rt.string,
});
const AssetDetailsUrlRT = rt.union([AssetDetailsUrlStateRT, rt.null]);
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_custom_dashboards.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_custom_dashboards.ts
new file mode 100644
index 0000000000000..e7e3f2eec3d87
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_custom_dashboards.ts
@@ -0,0 +1,211 @@
+/*
+ * 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 { useCallback } from 'react';
+import { fold } from 'fp-ts/lib/Either';
+import { identity } from 'fp-ts/lib/function';
+import { pipe } from 'fp-ts/lib/pipeable';
+import { i18n } from '@kbn/i18n';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
+import { useTrackedPromise } from '../../../utils/use_tracked_promise';
+import type {
+ InfraCustomDashboard,
+ InfraSavedCustomDashboard,
+ InfraCustomDashboardAssetType,
+} from '../../../../common/custom_dashboards';
+import {
+ InfraCustomDashboardRT,
+ InfraDeleteCustomDashboardsResponseBodyRT,
+} from '../../../../common/http_api/custom_dashboards_api';
+import { throwErrors, createPlainError } from '../../../../common/runtime_types';
+
+type ActionType = 'create' | 'update' | 'delete';
+const errorMessages: Record = {
+ create: i18n.translate('xpack.infra.linkDashboards.addFailure.toast.title', {
+ defaultMessage: 'Error while linking dashboards',
+ }),
+ update: i18n.translate('xpack.infra.updatingLinkedDashboards.addFailure.toast.title', {
+ defaultMessage: 'Error while updating linked dashboards',
+ }),
+ delete: i18n.translate('xpack.infra.deletingLinkedDashboards.addFailure.toast.title', {
+ defaultMessage: 'Error while deleting linked dashboards',
+ }),
+};
+
+const decodeResponse = (response: any) => {
+ return pipe(
+ InfraCustomDashboardRT.decode(response),
+ fold(throwErrors(createPlainError), identity)
+ );
+};
+
+export const useUpdateCustomDashboard = () => {
+ const { services } = useKibanaContextForPlugin();
+ const { notifications } = useKibana();
+
+ const onError = useCallback(
+ (errorMessage: string) => {
+ if (errorMessage) {
+ notifications.toasts.danger({
+ title: errorMessages.update,
+ body: errorMessage,
+ });
+ }
+ },
+ [notifications.toasts]
+ );
+
+ const [updateCustomDashboardRequest, updateCustomDashboard] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async ({
+ assetType,
+ id,
+ dashboardSavedObjectId,
+ dashboardFilterAssetIdEnabled,
+ }: InfraSavedCustomDashboard) => {
+ const rawResponse = await services.http.fetch(
+ `/api/infra/${assetType}/custom-dashboards/${id}`,
+ {
+ method: 'PUT',
+ body: JSON.stringify({
+ assetType,
+ dashboardSavedObjectId,
+ dashboardFilterAssetIdEnabled,
+ }),
+ }
+ );
+
+ return decodeResponse(rawResponse);
+ },
+ onResolve: (response) => response,
+ onReject: (e: Error | unknown) => onError((e as Error)?.message),
+ },
+ []
+ );
+
+ const isUpdateLoading = updateCustomDashboardRequest.state === 'pending';
+
+ const hasUpdateError = updateCustomDashboardRequest.state === 'rejected';
+
+ return {
+ updateCustomDashboard,
+ isUpdateLoading,
+ hasUpdateError,
+ };
+};
+
+export const useDeleteCustomDashboard = () => {
+ const { services } = useKibanaContextForPlugin();
+ const { notifications } = useKibana();
+
+ const decodeDeleteResponse = (response: any) => {
+ return pipe(
+ InfraDeleteCustomDashboardsResponseBodyRT.decode(response),
+ fold(throwErrors(createPlainError), identity)
+ );
+ };
+
+ const onError = useCallback(
+ (errorMessage: string) => {
+ if (errorMessage) {
+ notifications.toasts.danger({
+ title: errorMessages.delete,
+ body: errorMessage,
+ });
+ }
+ },
+ [notifications.toasts]
+ );
+
+ const [deleteCustomDashboardRequest, deleteCustomDashboard] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async ({
+ assetType,
+ id,
+ }: {
+ assetType: InfraCustomDashboardAssetType;
+ id: string;
+ }) => {
+ const rawResponse = await services.http.fetch(
+ `/api/infra/${assetType}/custom-dashboards/${id}`,
+ {
+ method: 'DELETE',
+ }
+ );
+
+ return decodeDeleteResponse(rawResponse);
+ },
+ onResolve: (response) => response,
+ onReject: (e: Error | unknown) => onError((e as Error)?.message),
+ },
+ []
+ );
+
+ const isDeleteLoading = deleteCustomDashboardRequest.state === 'pending';
+
+ const hasDeleteError = deleteCustomDashboardRequest.state === 'rejected';
+
+ return {
+ deleteCustomDashboard,
+ isDeleteLoading,
+ hasDeleteError,
+ };
+};
+
+export const useCreateCustomDashboard = () => {
+ const { services } = useKibanaContextForPlugin();
+ const { notifications } = useKibana();
+
+ const onError = useCallback(
+ (errorMessage: string) => {
+ if (errorMessage) {
+ notifications.toasts.danger({
+ title: errorMessages.delete,
+ body: errorMessage,
+ });
+ }
+ },
+ [notifications.toasts]
+ );
+
+ const [createCustomDashboardRequest, createCustomDashboard] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async ({
+ assetType,
+ dashboardSavedObjectId,
+ dashboardFilterAssetIdEnabled,
+ }: InfraCustomDashboard) => {
+ const rawResponse = await services.http.fetch(`/api/infra/${assetType}/custom-dashboards`, {
+ method: 'POST',
+ body: JSON.stringify({
+ dashboardSavedObjectId,
+ dashboardFilterAssetIdEnabled,
+ }),
+ });
+
+ return decodeResponse(rawResponse);
+ },
+ onResolve: (response) => response,
+ onReject: (e: Error | unknown) => onError((e as Error)?.message),
+ },
+ []
+ );
+
+ const isCreateLoading = createCustomDashboardRequest.state === 'pending';
+
+ const hasCreateError = createCustomDashboardRequest.state === 'rejected';
+
+ return {
+ createCustomDashboard,
+ isCreateLoading,
+ hasCreateError,
+ };
+};
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_dashboards_fetcher.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_dashboards_fetcher.ts
new file mode 100644
index 0000000000000..52cce13163361
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_dashboards_fetcher.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 type { SearchDashboardsResponse } from '@kbn/dashboard-plugin/public/services/dashboard_content_management/lib/find_dashboards';
+import { i18n } from '@kbn/i18n';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
+
+export enum FETCH_STATUS {
+ LOADING = 'loading',
+ SUCCESS = 'success',
+ FAILURE = 'failure',
+ NOT_INITIATED = 'not_initiated',
+}
+
+export interface SearchDashboardsResult {
+ data: SearchDashboardsResponse['hits'];
+ status: FETCH_STATUS;
+}
+
+export function useDashboardFetcher(query = ''): SearchDashboardsResult {
+ const {
+ services: { dashboard },
+ } = useKibanaContextForPlugin();
+ const { notifications } = 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 (error) {
+ setResult({
+ data: [],
+ status: FETCH_STATUS.FAILURE,
+ });
+ notifications.toasts.danger({
+ title: i18n.translate('xpack.infra.fetchingDashboards.addFailure.toast.title', {
+ defaultMessage: 'Error while fetching dashboards',
+ }),
+ body: error.message,
+ });
+ }
+ };
+ getDashboards();
+ }, [dashboard, notifications.toasts, query]);
+ return result;
+}
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_fetch_custom_dashboards.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_fetch_custom_dashboards.ts
new file mode 100644
index 0000000000000..c285aa9a931bd
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_fetch_custom_dashboards.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 { useEffect } from 'react';
+import { fold } from 'fp-ts/lib/Either';
+import { identity } from 'fp-ts/lib/function';
+import { pipe } from 'fp-ts/lib/pipeable';
+import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common';
+import type { InfraSavedCustomDashboard } from '../../../../common/custom_dashboards';
+import { InfraGetCustomDashboardsResponseBodyRT } from '../../../../common/http_api/custom_dashboards_api';
+import { useHTTPRequest } from '../../../hooks/use_http_request';
+import { throwErrors, createPlainError } from '../../../../common/runtime_types';
+import { useRequestObservable } from './use_request_observable';
+
+interface UseDashboardProps {
+ assetType: InventoryItemType;
+}
+
+export function useFetchCustomDashboards({ assetType }: UseDashboardProps) {
+ const { request$ } = useRequestObservable();
+
+ const decodeResponse = (response: any) => {
+ return pipe(
+ InfraGetCustomDashboardsResponseBodyRT.decode(response),
+ fold(throwErrors(createPlainError), identity)
+ );
+ };
+
+ const { error, loading, response, makeRequest } = useHTTPRequest(
+ `/api/infra/${assetType}/custom-dashboards`,
+ 'GET',
+ undefined,
+ decodeResponse,
+ undefined,
+ undefined,
+ true
+ );
+
+ useEffect(() => {
+ if (request$) {
+ request$.next(makeRequest);
+ } else {
+ makeRequest();
+ }
+ }, [makeRequest, request$]);
+
+ return {
+ error: (error && error.message) || null,
+ loading,
+ dashboards: response,
+ reload: makeRequest,
+ };
+}
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx
index fc4577227f615..c13bffae7ac9f 100644
--- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx
@@ -12,6 +12,8 @@ import {
type EuiPageHeaderProps,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
+import { useUiSetting } from '@kbn/kibana-react-plugin/public';
+import { enableInfrastructureAssetCustomDashboards } from '@kbn/observability-plugin/common';
import { useLinkProps } from '@kbn/observability-shared-plugin/public';
import { capitalize } from 'lodash';
import React, { useCallback, useMemo } from 'react';
@@ -111,13 +113,17 @@ const useRightSideItems = (links?: LinkOptions[]) => {
const useFeatureFlagTabs = () => {
const { featureFlags } = usePluginConfig();
const isProfilingEnabled = useProfilingIntegrationSetting();
+ const isInfrastructureAssetCustomDashboardsEnabled = useUiSetting(
+ enableInfrastructureAssetCustomDashboards
+ );
const featureFlagControlledTabs: Partial> = useMemo(
() => ({
[ContentTabIds.OSQUERY]: featureFlags.osqueryEnabled,
[ContentTabIds.PROFILING]: isProfilingEnabled,
+ [ContentTabIds.DASHBOARDS]: isInfrastructureAssetCustomDashboardsEnabled,
}),
- [featureFlags.osqueryEnabled, isProfilingEnabled]
+ [featureFlags.osqueryEnabled, isInfrastructureAssetCustomDashboardsEnabled, isProfilingEnabled]
);
const isTabEnabled = useCallback(
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/edit_dashboard.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/edit_dashboard.tsx
new file mode 100644
index 0000000000000..c5748dc641185
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/edit_dashboard.tsx
@@ -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 { EuiButtonEmpty } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React, { useState, useCallback } from 'react';
+import type {
+ DashboardItemWithTitle,
+ InfraCustomDashboardAssetType,
+} from '../../../../../../common/custom_dashboards';
+import { SaveDashboardModal } from './save_dashboard_modal';
+
+export function EditDashboard({
+ onRefresh,
+ currentDashboard,
+ assetType,
+}: {
+ onRefresh: () => void;
+ currentDashboard: DashboardItemWithTitle;
+ assetType: InfraCustomDashboardAssetType;
+}) {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const onClick = useCallback(() => setIsModalVisible(!isModalVisible), [isModalVisible]);
+
+ return (
+ <>
+
+ {i18n.translate('xpack.infra.customDashboards.editEmptyButtonLabel', {
+ defaultMessage: 'Edit dashboard link',
+ })}
+
+
+ {isModalVisible && (
+
+ )}
+ >
+ );
+}
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/goto_dashboard_link.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/goto_dashboard_link.tsx
new file mode 100644
index 0000000000000..562edc282131a
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/goto_dashboard_link.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { EuiButtonEmpty } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import type { DashboardItemWithTitle } from '../../../../../../common/custom_dashboards';
+import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana';
+
+export function GotoDashboardLink({
+ currentDashboard,
+}: {
+ currentDashboard: DashboardItemWithTitle;
+}) {
+ const {
+ services: {
+ dashboard: { locator: dashboardLocator },
+ },
+ } = useKibanaContextForPlugin();
+
+ const url = dashboardLocator?.getRedirectUrl({
+ dashboardId: currentDashboard?.dashboardSavedObjectId,
+ });
+ return (
+
+ {i18n.translate('xpack.infra.customDashboards.contextMenu.goToDashboard', {
+ defaultMessage: 'Go to dashboard',
+ })}
+
+ );
+}
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/index.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/index.ts
new file mode 100644
index 0000000000000..a7cac82c1a84f
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/index.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 { LinkDashboard } from './link_dashboard';
+export { GotoDashboardLink } from './goto_dashboard_link';
+export { EditDashboard } from './edit_dashboard';
+export { UnlinkDashboard } from './unlink_dashboard';
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/link_dashboard.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/link_dashboard.tsx
new file mode 100644
index 0000000000000..336f418a583f6
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/link_dashboard.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React, { useState, useCallback } from 'react';
+import type {
+ DashboardItemWithTitle,
+ InfraCustomDashboardAssetType,
+} from '../../../../../../common/custom_dashboards';
+import { SaveDashboardModal } from './save_dashboard_modal';
+
+export function LinkDashboard({
+ onRefresh,
+ newDashboardButton = false,
+ customDashboards,
+ assetType,
+}: {
+ onRefresh: () => void;
+ newDashboardButton?: boolean;
+ customDashboards?: DashboardItemWithTitle[];
+ assetType: InfraCustomDashboardAssetType;
+}) {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+
+ const onClick = useCallback(() => setIsModalVisible(true), []);
+ const onClose = useCallback(() => setIsModalVisible(false), []);
+
+ return (
+ <>
+ {newDashboardButton ? (
+
+ {i18n.translate('xpack.infra.assetDetails.dashboards.linkNewDashboardButtonLabel', {
+ defaultMessage: 'Link new dashboard',
+ })}
+
+ ) : (
+
+ {i18n.translate('xpack.infra.assetDetails.dashboards.linkButtonLabel', {
+ defaultMessage: 'Link dashboard',
+ })}
+
+ )}
+ {isModalVisible && (
+
+ )}
+ >
+ );
+}
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/save_dashboard_modal.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/save_dashboard_modal.tsx
new file mode 100644
index 0000000000000..bcc49694ccd32
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/save_dashboard_modal.tsx
@@ -0,0 +1,275 @@
+/*
+ * 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, useMemo, useState } from 'react';
+import {
+ EuiButton,
+ EuiModal,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiSwitch,
+ EuiModalBody,
+ EuiComboBox,
+ EuiComboBoxOptionOption,
+ EuiFlexGroup,
+ EuiToolTip,
+ EuiIcon,
+ EuiButtonEmpty,
+ useEuiTheme,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { DashboardItem } from '@kbn/dashboard-plugin/common/content_management';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import type {
+ DashboardItemWithTitle,
+ InfraCustomDashboardAssetType,
+} from '../../../../../../common/custom_dashboards';
+import { useDashboardFetcher, FETCH_STATUS } from '../../../hooks/use_dashboards_fetcher';
+import {
+ useUpdateCustomDashboard,
+ useCreateCustomDashboard,
+} from '../../../hooks/use_custom_dashboards';
+import { useAssetDetailsUrlState } from '../../../hooks/use_asset_details_url_state';
+
+interface Props {
+ onClose: () => void;
+ onRefresh: () => void;
+ currentDashboard?: DashboardItemWithTitle;
+ customDashboards?: DashboardItemWithTitle[];
+ assetType: InfraCustomDashboardAssetType;
+}
+
+export function SaveDashboardModal({
+ onClose,
+ onRefresh,
+ currentDashboard,
+ customDashboards,
+ assetType,
+}: Props) {
+ const { notifications } = useKibana();
+ const { data: allAvailableDashboards, status } = useDashboardFetcher();
+ const [, setUrlState] = useAssetDetailsUrlState();
+ const { euiTheme } = useEuiTheme();
+
+ const [assetNameEnabled, setAssetNameFiltersEnabled] = useState(
+ currentDashboard?.dashboardFilterAssetIdEnabled ?? true
+ );
+ const [selectedDashboard, setSelectedDashboard] = useState<
+ Array>
+ >(
+ currentDashboard
+ ? [{ label: currentDashboard.title, value: currentDashboard.dashboardSavedObjectId }]
+ : []
+ );
+
+ const { isUpdateLoading, updateCustomDashboard } = useUpdateCustomDashboard();
+ const { isCreateLoading, createCustomDashboard } = useCreateCustomDashboard();
+
+ const isEditMode = !!currentDashboard?.id;
+ const loading = isUpdateLoading || isCreateLoading;
+
+ const options = useMemo(
+ () =>
+ allAvailableDashboards?.map((dashboardItem: DashboardItem) => ({
+ label: dashboardItem.attributes.title,
+ value: dashboardItem.id,
+ disabled:
+ customDashboards?.some(
+ ({ dashboardSavedObjectId }) => dashboardItem.id === dashboardSavedObjectId
+ ) ?? false,
+ })),
+ [allAvailableDashboards, customDashboards]
+ );
+
+ const onChange = useCallback(
+ () => setAssetNameFiltersEnabled(!assetNameEnabled),
+ [assetNameEnabled]
+ );
+ const onSelect = useCallback((newSelection) => setSelectedDashboard(newSelection), []);
+
+ const onClickSave = useCallback(
+ async function () {
+ const [newDashboard] = selectedDashboard;
+ try {
+ if (!newDashboard.value) {
+ return;
+ }
+
+ const dashboardParams = {
+ assetType,
+ dashboardSavedObjectId: newDashboard.value,
+ dashboardFilterAssetIdEnabled: assetNameEnabled,
+ };
+
+ const result =
+ isEditMode && currentDashboard?.id
+ ? await updateCustomDashboard({
+ ...dashboardParams,
+ id: currentDashboard.id,
+ })
+ : await createCustomDashboard(dashboardParams);
+
+ const getToastLabels = isEditMode ? getEditSuccessToastLabels : getLinkSuccessToastLabels;
+
+ if (result && !(isEditMode ? isUpdateLoading : isCreateLoading)) {
+ notifications.toasts.success(getToastLabels(newDashboard.label));
+ }
+
+ setUrlState({ dashboardId: newDashboard.value });
+ onRefresh();
+ } catch (error) {
+ notifications.toasts.danger({
+ title: i18n.translate('xpack.infra.customDashboards.addFailure.toast.title', {
+ defaultMessage: 'Error while adding "{dashboardName}" dashboard',
+ values: { dashboardName: newDashboard.label },
+ }),
+ body: error.message,
+ });
+ }
+ onClose();
+ },
+ [
+ selectedDashboard,
+ onClose,
+ isEditMode,
+ setUrlState,
+ onRefresh,
+ updateCustomDashboard,
+ assetType,
+ currentDashboard?.id,
+ assetNameEnabled,
+ isUpdateLoading,
+ notifications.toasts,
+ createCustomDashboard,
+ isCreateLoading,
+ ]
+ );
+
+ return (
+
+
+
+ {isEditMode
+ ? i18n.translate('xpack.infra.customDashboards.selectDashboard.modalTitle.edit', {
+ defaultMessage: 'Edit dashboard',
+ })
+ : i18n.translate('xpack.infra.customDashboards.selectDashboard.modalTitle.link', {
+ defaultMessage: 'Select dashboard',
+ })}
+
+
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.infra.customDashboard.addDashboard.useContextFilterLabel',
+ {
+ defaultMessage: 'Filter by host name',
+ }
+ )}
+
+
+
+
+
+ }
+ onChange={onChange}
+ checked={assetNameEnabled}
+ />
+
+
+
+
+
+ {i18n.translate('xpack.infra.customDashboards.selectDashboard.cancel', {
+ defaultMessage: 'Cancel',
+ })}
+
+
+ {isEditMode
+ ? i18n.translate('xpack.infra.customDashboards.selectDashboard.edit', {
+ defaultMessage: 'Save',
+ })
+ : i18n.translate('xpack.infra.customDashboards.selectDashboard.add', {
+ defaultMessage: 'Link dashboard',
+ })}
+
+
+
+ );
+}
+
+function getLinkSuccessToastLabels(dashboardName: string) {
+ return {
+ title: i18n.translate('xpack.infra.customDashboards.linkSuccess.toast.title', {
+ defaultMessage: 'Added "{dashboardName}" dashboard',
+ values: { dashboardName },
+ }),
+ body: i18n.translate('xpack.infra.customDashboards.linkSuccess.toast.text', {
+ defaultMessage: 'Your dashboard is now visible in the asset details page.',
+ }),
+ };
+}
+
+function getEditSuccessToastLabels(dashboardName: string) {
+ return {
+ title: i18n.translate('xpack.infra.customDashboards.editSuccess.toast.title', {
+ defaultMessage: 'Edited "{dashboardName}" dashboard',
+ values: { dashboardName },
+ }),
+ body: i18n.translate('xpack.infra.customDashboards.editSuccess.toast.text', {
+ defaultMessage: 'Your dashboard link has been updated',
+ }),
+ };
+}
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/unlink_dashboard.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/unlink_dashboard.tsx
new file mode 100644
index 0000000000000..f29b6f57e670a
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/actions/unlink_dashboard.tsx
@@ -0,0 +1,129 @@
+/*
+ * 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 { useKibana } from '@kbn/kibana-react-plugin/public';
+import type {
+ DashboardItemWithTitle,
+ InfraCustomDashboardAssetType,
+} from '../../../../../../common/custom_dashboards';
+import { useDeleteCustomDashboard } from '../../../hooks/use_custom_dashboards';
+import { useFetchCustomDashboards } from '../../../hooks/use_fetch_custom_dashboards';
+import { useAssetDetailsUrlState } from '../../../hooks/use_asset_details_url_state';
+
+export function UnlinkDashboard({
+ currentDashboard,
+ onRefresh,
+ assetType,
+}: {
+ currentDashboard: DashboardItemWithTitle;
+ onRefresh: () => void;
+ assetType: InfraCustomDashboardAssetType;
+}) {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const { notifications } = useKibana();
+
+ const [, setUrlState] = useAssetDetailsUrlState();
+ const { deleteCustomDashboard, isDeleteLoading } = useDeleteCustomDashboard();
+ const { dashboards, loading } = useFetchCustomDashboards({ assetType });
+
+ const onClick = useCallback(() => setIsModalVisible(true), []);
+ const onCancel = useCallback(() => setIsModalVisible(false), []);
+ const onError = useCallback(() => setIsModalVisible(!isModalVisible), [isModalVisible]);
+
+ const onConfirm = useCallback(
+ async function () {
+ try {
+ const linkedDashboards = (dashboards ?? []).filter(
+ ({ dashboardSavedObjectId }) =>
+ dashboardSavedObjectId !== currentDashboard.dashboardSavedObjectId
+ );
+ const result = await deleteCustomDashboard({
+ assetType,
+ id: currentDashboard.id,
+ });
+ setUrlState({ dashboardId: linkedDashboards[0]?.dashboardSavedObjectId });
+
+ if (result) {
+ notifications.toasts.success({
+ title: i18n.translate('xpack.infra.customDashboards.unlinkSuccess.toast.title', {
+ defaultMessage: 'Unlinked "{dashboardName}" dashboard',
+ values: { dashboardName: currentDashboard?.title },
+ }),
+ });
+ onRefresh();
+ }
+ } catch (error) {
+ notifications.toasts.danger({
+ title: i18n.translate('xpack.infra.customDashboards.unlinkFailure.toast.title', {
+ defaultMessage: 'Error while unlinking "{dashboardName}" dashboard',
+ values: { dashboardName: currentDashboard?.title },
+ }),
+ body: error.body.message,
+ });
+ }
+ onError();
+ },
+ [
+ onError,
+ dashboards,
+ deleteCustomDashboard,
+ assetType,
+ currentDashboard.id,
+ currentDashboard.dashboardSavedObjectId,
+ currentDashboard?.title,
+ setUrlState,
+ notifications.toasts,
+ onRefresh,
+ ]
+ );
+
+ return (
+ <>
+
+ {i18n.translate('xpack.infra.customDashboards.unlinkEmptyButtonLabel', {
+ defaultMessage: 'Unlink dashboard',
+ })}
+
+ {isModalVisible && (
+
+
+ {i18n.translate('xpack.infra.customDashboards.unlinkEmptyButtonLabel.confirm.body', {
+ defaultMessage: `You are about to unlink the dashboard from the {assetType} context`,
+ values: { assetType },
+ })}
+
+
+ )}
+ >
+ );
+}
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/context_menu.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/context_menu.tsx
new file mode 100644
index 0000000000000..d9148633a4b0d
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/context_menu.tsx
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { i18n } from '@kbn/i18n';
+
+import React from 'react';
+import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui';
+import { useBoolean } from '../../../../hooks/use_boolean';
+
+interface Props {
+ items: React.ReactNode[];
+}
+
+export function ContextMenu({ items }: Props) {
+ const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false);
+
+ return (
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={closePopover}
+ panelPaddingSize="none"
+ anchorPosition="downLeft"
+ >
+ (
+
+ {item}
+
+ ))}
+ />
+
+ );
+}
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboard_selector.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboard_selector.tsx
new file mode 100644
index 0000000000000..e8ff7000581da
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboard_selector.tsx
@@ -0,0 +1,88 @@
+/*
+ * 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, useState, useCallback } from 'react';
+import useMount from 'react-use/lib/useMount';
+import { EuiComboBox } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { DashboardItemWithTitle } from '../../../../../common/custom_dashboards';
+import { useAssetDetailsUrlState } from '../../hooks/use_asset_details_url_state';
+
+interface Props {
+ customDashboards: DashboardItemWithTitle[];
+ currentDashboardId?: string;
+ setCurrentDashboard: (newDashboard: DashboardItemWithTitle) => void;
+ onRefresh: () => void;
+}
+
+export function DashboardSelector({
+ customDashboards,
+ currentDashboardId,
+ setCurrentDashboard,
+ onRefresh,
+}: Props) {
+ const [, setUrlState] = useAssetDetailsUrlState();
+
+ const [selectedDashboard, setSelectedDashboard] = useState();
+
+ useMount(() => {
+ if (!currentDashboardId) {
+ setUrlState({ dashboardId: customDashboards[0]?.dashboardSavedObjectId });
+ }
+ });
+
+ useEffect(() => {
+ const preselectedDashboard = customDashboards.find(
+ ({ dashboardSavedObjectId }) => dashboardSavedObjectId === currentDashboardId
+ );
+ // preselect dashboard
+ if (preselectedDashboard) {
+ setSelectedDashboard(preselectedDashboard);
+ setCurrentDashboard(preselectedDashboard);
+ }
+ }, [customDashboards, currentDashboardId, setCurrentDashboard]);
+
+ const onChange = useCallback(
+ (newDashboardId?: string) => {
+ setUrlState({ dashboardId: newDashboardId });
+ onRefresh();
+ },
+ [onRefresh, setUrlState]
+ );
+
+ return (
+ {
+ return {
+ label: title,
+ value: dashboardSavedObjectId,
+ };
+ })}
+ selectedOptions={
+ selectedDashboard
+ ? [
+ {
+ value: selectedDashboard.dashboardSavedObjectId,
+ label: selectedDashboard.title,
+ },
+ ]
+ : []
+ }
+ onChange={([newItem]) => onChange(newItem.value)}
+ isClearable={false}
+ />
+ );
+}
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboards.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboards.tsx
new file mode 100644
index 0000000000000..495cc306f02b8
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboards.tsx
@@ -0,0 +1,209 @@
+/*
+ * 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 type { DashboardItem } from '@kbn/dashboard-plugin/common/content_management';
+import { buildAssetIdFilter } from '../../../../utils/filters/build';
+import type {
+ InfraSavedCustomDashboard,
+ DashboardItemWithTitle,
+} from '../../../../../common/custom_dashboards';
+
+import { EmptyDashboards } from './empty_dashboards';
+import { EditDashboard, GotoDashboardLink, LinkDashboard, UnlinkDashboard } from './actions';
+import { useFetchCustomDashboards } from '../../hooks/use_fetch_custom_dashboards';
+import { useDatePickerContext } from '../../hooks/use_date_picker';
+import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props';
+import { FETCH_STATUS, useDashboardFetcher } from '../../hooks/use_dashboards_fetcher';
+import { useDataViewsContext } from '../../hooks/use_data_views';
+import { DashboardSelector } from './dashboard_selector';
+import { ContextMenu } from './context_menu';
+import { useAssetDetailsUrlState } from '../../hooks/use_asset_details_url_state';
+
+export function Dashboards() {
+ const { dateRange } = useDatePickerContext();
+ const { asset } = useAssetDetailsRenderPropsContext();
+ const [dashboard, setDashboard] = useState();
+ const [customDashboards, setCustomDashboards] = useState([]);
+ const [currentDashboard, setCurrentDashboard] = useState();
+ const { data: allAvailableDashboards, status } = useDashboardFetcher();
+ const { metrics } = useDataViewsContext();
+ const [urlState, setUrlState] = useAssetDetailsUrlState();
+
+ const { dashboards, loading, reload } = useFetchCustomDashboards({ assetType: asset.type });
+
+ useEffect(() => {
+ const allAvailableDashboardsMap = new Map();
+ allAvailableDashboards.forEach((availableDashboard) => {
+ allAvailableDashboardsMap.set(availableDashboard.id, availableDashboard);
+ });
+ const filteredCustomDashboards =
+ dashboards?.reduce(
+ (result: DashboardItemWithTitle[], customDashboard: InfraSavedCustomDashboard) => {
+ const matchedDashboard = allAvailableDashboardsMap.get(
+ customDashboard.dashboardSavedObjectId
+ );
+ if (matchedDashboard) {
+ result.push({
+ title: matchedDashboard.attributes.title,
+ ...customDashboard,
+ });
+ }
+ return result;
+ },
+ []
+ ) ?? [];
+ setCustomDashboards(filteredCustomDashboards);
+ // set a default dashboard if there is no selected dashboard
+ if (!urlState?.dashboardId) {
+ setUrlState({
+ dashboardId:
+ currentDashboard?.dashboardSavedObjectId ??
+ filteredCustomDashboards[0]?.dashboardSavedObjectId,
+ });
+ }
+ }, [
+ allAvailableDashboards,
+ currentDashboard?.dashboardSavedObjectId,
+ dashboards,
+ setUrlState,
+ urlState?.dashboardId,
+ ]);
+
+ const getCreationOptions = useCallback((): Promise => {
+ const getInitialInput = () => ({
+ viewMode: ViewMode.VIEW,
+ timeRange: { from: dateRange.from, to: dateRange.to },
+ });
+ return Promise.resolve({ getInitialInput });
+ }, [dateRange.from, dateRange.to]);
+
+ useEffect(() => {
+ if (!dashboard) return;
+ dashboard.updateInput({
+ filters:
+ metrics.dataView && currentDashboard?.dashboardFilterAssetIdEnabled
+ ? buildAssetIdFilter(asset.name, asset.type, metrics.dataView)
+ : [],
+ timeRange: { from: dateRange.from, to: dateRange.to },
+ });
+ }, [
+ metrics.dataView,
+ asset.name,
+ dashboard,
+ dateRange.from,
+ dateRange.to,
+ currentDashboard,
+ asset.type,
+ ]);
+
+ if (loading || status === FETCH_STATUS.LOADING) {
+ return (
+
+ }
+ title={
+
+ {i18n.translate('xpack.infra.customDashboards.loadingCustomDashboards', {
+ defaultMessage: 'Loading dashboard',
+ })}
+
+ }
+ />
+
+ );
+ }
+
+ return (
+
+ {!!dashboards?.length ? (
+ <>
+
+
+
+ {currentDashboard?.title}
+
+
+
+
+
+
+
+ {currentDashboard && (
+
+ ,
+ ,
+ ,
+ ,
+ ]}
+ />
+
+ )}
+
+
+
+ {urlState?.dashboardId && (
+
+ )}
+
+ >
+ ) : (
+
+ }
+ />
+ )}
+
+ );
+}
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/empty_dashboards.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/empty_dashboards.tsx
new file mode 100644
index 0000000000000..019818f6cc524
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/empty_dashboards.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import { EuiEmptyPrompt, EuiImage } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { dashboardsDark, dashboardsLight } from '@kbn/shared-svg';
+import { useIsDarkMode } from '../../../../hooks/use_is_dark_mode';
+
+interface Props {
+ actions: React.ReactNode;
+}
+
+export function EmptyDashboards({ actions }: Props) {
+ const isDarkMode = useIsDarkMode();
+
+ return (
+
+ }
+ title={
+
+ {i18n.translate('xpack.infra.assetDetails.dashboards.emptyTitle', {
+ defaultMessage: 'Want your own view?',
+ })}
+
+ }
+ layout="horizontal"
+ color="plain"
+ body={
+ <>
+
+
+ {i18n.translate('xpack.infra.assetDetails.dashboards.emptyBody.first', {
+ defaultMessage: 'Link your own dashboard to this view',
+ })}
+
+
+ {i18n.translate('xpack.infra.assetDetails.dashboards.emptyBody.second', {
+ defaultMessage: 'Provide the best visualizations relevant to your business',
+ })}
+
+
+ {i18n.translate('xpack.infra.assetDetails.dashboards.emptyBody', {
+ defaultMessage: 'Add or remove them at any time',
+ })}
+
+
+
+ {i18n.translate('xpack.infra.assetDetails.dashboards.emptyBody.getStarted', {
+ defaultMessage: 'To get started, add your dashboard',
+ })}
+
+ >
+ }
+ actions={actions}
+ />
+ );
+}
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/index.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/index.ts
index 779531d15910b..0fa4f76f13f7c 100644
--- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/index.ts
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/index.ts
@@ -12,3 +12,4 @@ export { Profiling } from './profiling/profiling';
export { Osquery } from './osquery/osquery';
export { Logs } from './logs/logs';
export { Overview } from './overview/overview';
+export { Dashboards } from './dashboards/dashboards';
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts
index 03ae24810e217..5e0124fa8a69c 100644
--- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts
@@ -26,6 +26,7 @@ export enum ContentTabIds {
OSQUERY = 'osquery',
LOGS = 'logs',
LINK_TO_APM = 'linkToApm',
+ DASHBOARDS = 'dashboards',
}
export type TabIds = `${ContentTabIds}`;
diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_saved_views_notifier.ts b/x-pack/plugins/observability_solution/infra/public/hooks/use_saved_views_notifier.ts
index b7b8822e3ec14..54035bce5f1b1 100644
--- a/x-pack/plugins/observability_solution/infra/public/hooks/use_saved_views_notifier.ts
+++ b/x-pack/plugins/observability_solution/infra/public/hooks/use_saved_views_notifier.ts
@@ -17,7 +17,7 @@ export const useSavedViewsNotifier = () => {
title:
message ||
i18n.translate('xpack.infra.savedView.errorOnDelete.title', {
- defaultMessage: `An error occured deleting the view.`,
+ defaultMessage: `An error occurred deleting the view.`,
}),
});
};
@@ -39,7 +39,7 @@ export const useSavedViewsNotifier = () => {
title:
message ||
i18n.translate('xpack.infra.savedView.errorOnCreate.title', {
- defaultMessage: `An error occured saving view.`,
+ defaultMessage: `An error occurred saving view.`,
}),
});
};
diff --git a/x-pack/plugins/observability_solution/infra/public/types.ts b/x-pack/plugins/observability_solution/infra/public/types.ts
index a3c43378a565b..3ef3dbfdef6b2 100644
--- a/x-pack/plugins/observability_solution/infra/public/types.ts
+++ b/x-pack/plugins/observability_solution/infra/public/types.ts
@@ -48,6 +48,7 @@ import { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assis
import type { CloudSetup } from '@kbn/cloud-plugin/public';
import type { LicenseManagementUIPluginSetup } from '@kbn/license-management-plugin/public';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
+import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import type { UnwrapPromise } from '../common/utility_types';
import { InventoryViewsServiceStart } from './services/inventory_views';
import { MetricsExplorerViewsServiceStart } from './services/metrics_explorer_views';
@@ -90,6 +91,7 @@ export interface InfraClientStartDeps {
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
discover: DiscoverStart;
+ dashboard: DashboardStart;
embeddable?: EmbeddableStart;
lens: LensPublicStart;
logsShared: LogsSharedClientStartExports;
diff --git a/x-pack/plugins/observability_solution/infra/public/utils/filters/build.test.ts b/x-pack/plugins/observability_solution/infra/public/utils/filters/build.test.ts
new file mode 100644
index 0000000000000..44e06bd415d32
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/utils/filters/build.test.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 { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub';
+import { buildAssetIdFilter } from './build';
+
+describe('buildAssetIdFilter', function () {
+ it('should build a host id filter', () => {
+ dataView.getFieldByName = jest.fn().mockReturnValue({
+ name: 'host.id',
+ });
+ const result = buildAssetIdFilter('host1', 'host', dataView);
+ expect(result[0]).toMatchObject({ query: { match_phrase: { 'host.id': 'host1' } } });
+ });
+
+ it('should build a pod id filter', () => {
+ dataView.getFieldByName = jest.fn().mockReturnValue({
+ name: 'kubernetes.pod.uid',
+ });
+ const result = buildAssetIdFilter('pod1', 'pod', dataView);
+ expect(result[0]).toMatchObject({ query: { match_phrase: { 'kubernetes.pod.uid': 'pod1' } } });
+ });
+
+ it('should return an empty array if the field id is not found', () => {
+ dataView.getFieldByName = jest.fn().mockReturnValue(undefined);
+ const result = buildAssetIdFilter('host1', 'host', dataView);
+ expect(result).toStrictEqual([]);
+ });
+});
diff --git a/x-pack/plugins/observability_solution/infra/public/utils/filters/build.ts b/x-pack/plugins/observability_solution/infra/public/utils/filters/build.ts
index 45879017a5943..b52804390bc98 100644
--- a/x-pack/plugins/observability_solution/infra/public/utils/filters/build.ts
+++ b/x-pack/plugins/observability_solution/infra/public/utils/filters/build.ts
@@ -9,10 +9,12 @@ import {
BooleanRelation,
buildCombinedFilter,
buildPhraseFilter,
- Filter,
+ type Filter,
isCombinedFilter,
} from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/common';
+import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common';
+import type { InfraCustomDashboardAssetType } from '../../../common/custom_dashboards';
export const buildCombinedHostsFilter = ({
field,
@@ -52,3 +54,12 @@ export const retrieveFieldsFromFilter = (filters: Filter[], fields: string[] = [
return fields;
};
+
+export const buildAssetIdFilter = (
+ assetId: string,
+ assetType: InfraCustomDashboardAssetType,
+ dataView: DataView
+): Filter[] => {
+ const assetIdField = dataView.getFieldByName(findInventoryFields(assetType).id);
+ return assetIdField ? [buildPhraseFilter(assetIdField, assetId, dataView)] : [];
+};
diff --git a/x-pack/plugins/observability_solution/infra/tsconfig.json b/x-pack/plugins/observability_solution/infra/tsconfig.json
index 0e5734b31a122..bf079001de494 100644
--- a/x-pack/plugins/observability_solution/infra/tsconfig.json
+++ b/x-pack/plugins/observability_solution/infra/tsconfig.json
@@ -94,6 +94,8 @@
"@kbn/serverless",
"@kbn/core-lifecycle-server",
"@kbn/elastic-agent-utils",
+ "@kbn/dashboard-plugin",
+ "@kbn/shared-svg",
"@kbn/aiops-log-rate-analysis"
],
"exclude": [
diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts
index b452241739163..9ad337dd5596f 100644
--- a/x-pack/test/functional/apps/infra/hosts_view.ts
+++ b/x-pack/test/functional/apps/infra/hosts_view.ts
@@ -7,10 +7,13 @@
import moment from 'moment';
import expect from '@kbn/expect';
-import { enableInfrastructureHostsView } from '@kbn/observability-plugin/common';
+import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
+import {
+ enableInfrastructureAssetCustomDashboards,
+ enableInfrastructureHostsView,
+} from '@kbn/observability-plugin/common';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services';
-import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { FtrProviderContext } from '../../ftr_provider_context';
import {
DATES,
@@ -121,6 +124,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const setHostViewEnabled = (value: boolean = true) =>
kibanaServer.uiSettings.update({ [enableInfrastructureHostsView]: value });
+ const setCustomDashboardsEnabled = (value: boolean = true) =>
+ kibanaServer.uiSettings.update({ [enableInfrastructureAssetCustomDashboards]: value });
+
const returnTo = async (path: string, timeout = 2000) =>
retry.waitForWithTimeout('returned to hosts view', timeout, async () => {
await browser.goBack();
@@ -189,6 +195,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
describe('#Single Host Flyout', () => {
before(async () => {
await setHostViewEnabled(true);
+ await setCustomDashboardsEnabled(true);
await pageObjects.common.navigateToApp(HOSTS_VIEW_PATH);
await pageObjects.header.waitUntilLoadingHasFinished();
});
@@ -318,6 +325,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
+ describe('Dashboards Tab', () => {
+ before(async () => {
+ await pageObjects.assetDetails.clickDashboardsTab();
+ });
+
+ it('should render dashboards tab splash screen with option to add dashboard', async () => {
+ await pageObjects.assetDetails.addDashboardExists();
+ });
+ });
+
describe('Flyout links', () => {
it('should navigate to Host Details page after click', async () => {
await pageObjects.assetDetails.clickOpenAsPageLink();
diff --git a/x-pack/test/functional/page_objects/asset_details.ts b/x-pack/test/functional/page_objects/asset_details.ts
index ce1bbe525d042..9fb4fbc7cfe39 100644
--- a/x-pack/test/functional/page_objects/asset_details.ts
+++ b/x-pack/test/functional/page_objects/asset_details.ts
@@ -254,6 +254,15 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) {
return testSubjects.click('infraAssetDetailsOsqueryTab');
},
+ // Dashboards
+ async clickDashboardsTab() {
+ return testSubjects.click('infraAssetDetailsDashboardsTab');
+ },
+
+ async addDashboardExists() {
+ await testSubjects.existOrFail('infraAddDashboard');
+ },
+
// APM Tab link
async clickApmTabLink() {
return testSubjects.click('infraAssetDetailsApmServicesLinkTab');
From f469fc2940dfceeb5624936aea39de2ce24dffdc Mon Sep 17 00:00:00 2001
From: Katerina
Date: Wed, 10 Apr 2024 14:49:01 +0300
Subject: [PATCH 05/55] [APM] Format comparison value for all overview mobile
metrics (#180353)
## Summary
Fixes https://github.com/elastic/kibana/issues/174551
### before
![Screenshot 2024-04-09 at 12 10
11](https://github.com/elastic/kibana/assets/3369346/4ebc02f0-f2ff-43e6-877b-27756272a5ab)
### after
![Screenshot 2024-04-09 at 12 23
49](https://github.com/elastic/kibana/assets/3369346/990f08da-8e7a-40db-9fdd-708c19fa2c3a)
---
.../components/app/mobile/service_overview/stats/stats.tsx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/mobile/service_overview/stats/stats.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/mobile/service_overview/stats/stats.tsx
index e75b2ffe38aa9..89efdc87ec7cd 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/mobile/service_overview/stats/stats.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/mobile/service_overview/stats/stats.tsx
@@ -73,7 +73,9 @@ export function MobileStats({
return (
{value && comparisonEnabled
- ? `${previousPeriodLabel}: ${value}`
+ ? `${previousPeriodLabel}: ${
+ Number.isInteger(value) ? value : value.toFixed(2)
+ }`
: null}
);
@@ -132,7 +134,7 @@ export function MobileStats({
: valueFormatter(Number(value.toFixed(1)), 'ms'),
trend: data?.currentPeriod?.launchTimes?.timeseries ?? [],
extra: getComparisonValueFormatter(
- data?.previousPeriod.launchTimes?.value?.toFixed(1)
+ data?.previousPeriod.launchTimes?.value
),
trendShape: MetricTrendShape.Area,
},
From eb92484b6cb9c933f41375d1c39a2fac70a70023 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Loix?=
Date: Wed, 10 Apr 2024 12:49:47 +0100
Subject: [PATCH 06/55] [Serverless] Remove visualisation link from side navs
(#180387)
---
.../public/navigation_tree.ts | 13 -------------
.../serverless_search/public/navigation_tree.ts | 13 -------------
x-pack/plugins/translations/translations/fr-FR.json | 4 +---
x-pack/plugins/translations/translations/ja-JP.json | 4 +---
x-pack/plugins/translations/translations/zh-CN.json | 4 +---
.../test_suites/search/default_dataview.ts | 9 ---------
6 files changed, 3 insertions(+), 44 deletions(-)
diff --git a/x-pack/plugins/serverless_observability/public/navigation_tree.ts b/x-pack/plugins/serverless_observability/public/navigation_tree.ts
index b65cbb3437efa..a3421c65bda38 100644
--- a/x-pack/plugins/serverless_observability/public/navigation_tree.ts
+++ b/x-pack/plugins/serverless_observability/public/navigation_tree.ts
@@ -48,19 +48,6 @@ export const navigationTree: NavigationTreeDefinition = {
return pathNameSerialized.startsWith(prepend('/app/dashboards'));
},
},
- {
- title: i18n.translate('xpack.serverlessObservability.nav.visualizations', {
- defaultMessage: 'Visualizations',
- }),
- link: 'visualize',
- getIsActive: ({ pathNameSerialized, prepend }) => {
- return (
- pathNameSerialized.startsWith(prepend('/app/visualize')) ||
- pathNameSerialized.startsWith(prepend('/app/lens')) ||
- pathNameSerialized.startsWith(prepend('/app/maps'))
- );
- },
- },
{
link: 'observability-overview:alerts',
},
diff --git a/x-pack/plugins/serverless_search/public/navigation_tree.ts b/x-pack/plugins/serverless_search/public/navigation_tree.ts
index 17c00fcf71ae2..56d1709967580 100644
--- a/x-pack/plugins/serverless_search/public/navigation_tree.ts
+++ b/x-pack/plugins/serverless_search/public/navigation_tree.ts
@@ -50,19 +50,6 @@ export const navigationTree: NavigationTreeDefinition = {
return pathNameSerialized.startsWith(prepend('/app/dashboards'));
},
},
- {
- link: 'visualize',
- title: i18n.translate('xpack.serverlessSearch.nav.visualize', {
- defaultMessage: 'Visualizations',
- }),
- getIsActive: ({ pathNameSerialized, prepend }) => {
- return (
- pathNameSerialized.startsWith(prepend('/app/visualize')) ||
- pathNameSerialized.startsWith(prepend('/app/lens')) ||
- pathNameSerialized.startsWith(prepend('/app/maps'))
- );
- },
- },
{
link: 'management:triggersActions',
title: i18n.translate('xpack.serverlessSearch.nav.alerts', {
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 30b08ea0f9dec..359026860d16f 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -38330,7 +38330,6 @@
"xpack.serverlessSearch.nav.mngt": "Gestion",
"xpack.serverlessSearch.nav.performance": "Performances",
"xpack.serverlessSearch.nav.projectSettings": "Paramètres de projet",
- "xpack.serverlessSearch.nav.visualize": "Visualisations",
"xpack.serverlessSearch.next": "Suivant",
"xpack.serverlessSearch.optional": "Facultatif",
"xpack.serverlessSearch.overview.footer.description": "Votre point de terminaison Elasticsearch est configuré et vous avez effectué quelques requêtes de base. Vous voilà prêt à approfondir les outils et les cas d'utilisation avancés.",
@@ -45116,7 +45115,6 @@
"xpack.serverlessObservability.nav.ml.jobs": "Détection des anomalies",
"xpack.serverlessObservability.nav.mngt": "Gestion",
"xpack.serverlessObservability.nav.projectSettings": "Paramètres de projet",
- "xpack.serverlessObservability.nav.synthetics": "Synthetics",
- "xpack.serverlessObservability.nav.visualizations": "Visualisations"
+ "xpack.serverlessObservability.nav.synthetics": "Synthetics"
}
}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index ab8b03488f6a5..2c3c3d2cf827b 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -38298,7 +38298,6 @@
"xpack.serverlessSearch.nav.mngt": "管理",
"xpack.serverlessSearch.nav.performance": "パフォーマンス",
"xpack.serverlessSearch.nav.projectSettings": "プロジェクト設定",
- "xpack.serverlessSearch.nav.visualize": "ビジュアライゼーション",
"xpack.serverlessSearch.next": "次へ",
"xpack.serverlessSearch.optional": "オプション",
"xpack.serverlessSearch.overview.footer.description": "Elasticsearchエンドポイントが設定され、いくつかの基本的なクエリが作成されました。これで、より高度なツールやユースケースを使いこなす準備が整いました。",
@@ -45086,7 +45085,6 @@
"xpack.serverlessObservability.nav.ml.jobs": "異常検知",
"xpack.serverlessObservability.nav.mngt": "管理",
"xpack.serverlessObservability.nav.projectSettings": "プロジェクト設定",
- "xpack.serverlessObservability.nav.synthetics": "Synthetics",
- "xpack.serverlessObservability.nav.visualizations": "ビジュアライゼーション"
+ "xpack.serverlessObservability.nav.synthetics": "Synthetics"
}
}
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 24a4d75a3af2c..448da6450b4d6 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -38342,7 +38342,6 @@
"xpack.serverlessSearch.nav.mngt": "管理",
"xpack.serverlessSearch.nav.performance": "性能",
"xpack.serverlessSearch.nav.projectSettings": "项目设置",
- "xpack.serverlessSearch.nav.visualize": "可视化",
"xpack.serverlessSearch.next": "下一步",
"xpack.serverlessSearch.optional": "可选",
"xpack.serverlessSearch.overview.footer.description": "已设置您的 Elasticsearch 终端,并且您已提出一些基本查询。现在您已准备就绪,可以更深入地了解更多高级工具和用例。",
@@ -45134,7 +45133,6 @@
"xpack.serverlessObservability.nav.ml.jobs": "异常检测",
"xpack.serverlessObservability.nav.mngt": "管理",
"xpack.serverlessObservability.nav.projectSettings": "项目设置",
- "xpack.serverlessObservability.nav.synthetics": "Synthetics",
- "xpack.serverlessObservability.nav.visualizations": "可视化"
+ "xpack.serverlessObservability.nav.synthetics": "Synthetics"
}
}
diff --git a/x-pack/test_serverless/functional/test_suites/search/default_dataview.ts b/x-pack/test_serverless/functional/test_suites/search/default_dataview.ts
index 2beb234f688f4..e2ec4c24e7016 100644
--- a/x-pack/test_serverless/functional/test_suites/search/default_dataview.ts
+++ b/x-pack/test_serverless/functional/test_suites/search/default_dataview.ts
@@ -43,14 +43,5 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
text: 'Editing New Dashboard',
});
});
-
- it('should show dashboard but with no data in visualize', async () => {
- await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'visualize' });
- await testSubjects.existOrFail('~breadcrumb-deepLinkId-visualize');
- await testSubjects.existOrFail('top-nav');
- await testSubjects.click('newItemButton');
- await testSubjects.existOrFail('visType-lens');
- await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Visualizations' });
- });
});
}
From ba2c27c95afb08936397e50be15d738a117b98f4 Mon Sep 17 00:00:00 2001
From: Kurt
Date: Wed, 10 Apr 2024 08:13:17 -0400
Subject: [PATCH 07/55] Upgrading express from 4.17.3 --> 4.19.2 (#180373)
## Summary
Upgrading `express` from 4.17.3. to 4.19.2
[Changelog](https://github.com/expressjs/express/compare/4.17.3...4.19.2)
---
yarn.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index bea08f4cd4b63..19de892abf9c0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -17027,9 +17027,9 @@ expr-eval@^2.0.2:
integrity sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==
express@^4.17.1, express@^4.17.3:
- version "4.17.3"
- resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1"
- integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==
+ version "4.19.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
+ integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
From 72c80ff74c504e555beed3d857f610e7e20e4e78 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Loix?=
Date: Wed, 10 Apr 2024 13:51:42 +0100
Subject: [PATCH 08/55] [Stateful sidenav] Put new nav behind launch darkly
flag (#179913)
---
.buildkite/ftr_configs.yml | 1 -
.../src/chrome_service.tsx | 1 -
.../project_navigation_service.test.ts | 19 --
.../project_navigation_service.ts | 18 +-
src/plugins/navigation/common/constants.ts | 6 +-
src/plugins/navigation/common/index.ts | 6 +-
src/plugins/navigation/kibana.jsonc | 2 +-
src/plugins/navigation/public/index.ts | 1 -
src/plugins/navigation/public/plugin.test.ts | 302 ++++++++++--------
src/plugins/navigation/public/plugin.tsx | 116 +++----
src/plugins/navigation/public/types.ts | 10 +-
src/plugins/navigation/server/config.ts | 7 +-
src/plugins/navigation/server/plugin.ts | 26 +-
src/plugins/navigation/server/types.ts | 13 +-
src/plugins/navigation/server/ui_settings.ts | 76 +----
src/plugins/navigation/tsconfig.json | 1 +
test/functional/apps/navigation/config.ts | 35 --
test/functional/apps/navigation/index.ts | 15 -
.../test_suites/core_plugins/rendering.ts | 2 -
.../cloud_experiments/common/constants.ts | 4 +
.../test/functional/apps/navigation/config.ts | 6 +-
.../functional/apps/navigation/tests/index.ts | 1 +
.../navigation/tests/solution_nav_switcher.ts | 8 +-
23 files changed, 276 insertions(+), 400 deletions(-)
delete mode 100644 test/functional/apps/navigation/config.ts
delete mode 100644 test/functional/apps/navigation/index.ts
rename test/functional/apps/navigation/_solution_nav_switcher.ts => x-pack/test/functional/apps/navigation/tests/solution_nav_switcher.ts (84%)
diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml
index ee6bb5ca8ffc1..4bca3d4b02b04 100644
--- a/.buildkite/ftr_configs.yml
+++ b/.buildkite/ftr_configs.yml
@@ -122,7 +122,6 @@ enabled:
- test/functional/apps/home/config.ts
- test/functional/apps/kibana_overview/config.ts
- test/functional/apps/management/config.ts
- - test/functional/apps/navigation/config.ts
- test/functional/apps/saved_objects_management/config.ts
- test/functional/apps/sharing/config.ts
- test/functional/apps/status_page/config.ts
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx
index 664230d749d0b..1d48fdbb24dc0 100644
--- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx
+++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx
@@ -245,7 +245,6 @@ export class ChromeService {
http,
chromeBreadcrumbs$: breadcrumbs$,
logger: this.logger,
- setChromeStyle,
});
const recentlyAccessed = await this.recentlyAccessed.start({ http });
const docTitle = this.docTitle.start();
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts
index 52e22f7aef03a..508e72c5b86b4 100644
--- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts
+++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts
@@ -60,11 +60,9 @@ const logger = loggerMock.create();
const setup = ({
locationPathName = '/',
navLinkIds,
- setChromeStyle = jest.fn(),
}: {
locationPathName?: string;
navLinkIds?: Readonly;
- setChromeStyle?: () => void;
} = {}) => {
const history = createMemoryHistory({
initialEntries: [locationPathName],
@@ -87,7 +85,6 @@ const setup = ({
http: httpServiceMock.createStartContract(),
chromeBreadcrumbs$,
logger,
- setChromeStyle,
});
return { projectNavigation, history, chromeBreadcrumbs$, navLinksService, application };
@@ -1018,22 +1015,6 @@ describe('solution navigations', () => {
);
});
- it('should set the Chrome style when the active solution navigation changes', async () => {
- const setChromeStyle = jest.fn();
- const { projectNavigation } = setup({ setChromeStyle });
-
- expect(setChromeStyle).not.toHaveBeenCalled();
-
- projectNavigation.updateSolutionNavigations({ 1: solution1, 2: solution2 });
- expect(setChromeStyle).not.toHaveBeenCalled();
-
- projectNavigation.changeActiveSolutionNavigation('2');
- expect(setChromeStyle).toHaveBeenCalledWith('project'); // We have an active solution nav, we should switch to project style
-
- projectNavigation.changeActiveSolutionNavigation(null);
- expect(setChromeStyle).toHaveBeenCalledWith('classic'); // No active solution, we should switch back to classic Kibana
- });
-
it('should change the active solution if no node match the current Location', async () => {
const { projectNavigation, navLinksService } = setup({
locationPathName: '/app/app3', // we are on app3 which only exists in solution3
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts
index 63675ea742aaf..59becdebeb406 100644
--- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts
+++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts
@@ -16,7 +16,6 @@ import type {
ChromeProjectNavigationNode,
NavigationTreeDefinition,
SolutionNavigationDefinitions,
- ChromeStyle,
CloudLinks,
} from '@kbn/core-chrome-browser';
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
@@ -57,7 +56,6 @@ interface StartDeps {
http: InternalHttpStart;
chromeBreadcrumbs$: Observable;
logger: Logger;
- setChromeStyle: (style: ChromeStyle) => void;
}
export class ProjectNavigationService {
@@ -91,23 +89,14 @@ export class ProjectNavigationService {
private http?: InternalHttpStart;
private navigationChangeSubscription?: Subscription;
private unlistenHistory?: () => void;
- private setChromeStyle: StartDeps['setChromeStyle'] = () => {};
-
- public start({
- application,
- navLinksService,
- http,
- chromeBreadcrumbs$,
- logger,
- setChromeStyle,
- }: StartDeps) {
+
+ public start({ application, navLinksService, http, chromeBreadcrumbs$, logger }: StartDeps) {
this.application = application;
this.navLinksService = navLinksService;
this.http = http;
this.logger = logger;
this.onHistoryLocationChange(application.history.location);
this.unlistenHistory = application.history.listen(this.onHistoryLocationChange.bind(this));
- this.setChromeStyle = setChromeStyle;
this.handleActiveNodesChange();
this.handleEmptyActiveNodes();
@@ -418,7 +407,6 @@ export class ProjectNavigationService {
// When we **do** have definitions, then passing `null` does mean we should change to "classic".
if (Object.keys(definitions).length > 0) {
if (id === null) {
- this.setChromeStyle('classic');
this.navigationTree$.next(undefined);
this.activeSolutionNavDefinitionId$.next(null);
} else {
@@ -427,8 +415,6 @@ export class ProjectNavigationService {
throw new Error(`Solution navigation definition with id "${id}" does not exist.`);
}
- this.setChromeStyle('project');
-
const { sideNavComponent } = definition;
if (sideNavComponent) {
this.setSideNavComponent(sideNavComponent);
diff --git a/src/plugins/navigation/common/constants.ts b/src/plugins/navigation/common/constants.ts
index 891b583b3ad84..be41e97416084 100644
--- a/src/plugins/navigation/common/constants.ts
+++ b/src/plugins/navigation/common/constants.ts
@@ -6,8 +6,6 @@
* Side Public License, v 1.
*/
-export const ENABLE_SOLUTION_NAV_UI_SETTING_ID = 'solutionNav:enable';
-
-export const OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID = 'solutionNav:optInStatus';
+export const SOLUTION_NAV_FEATURE_FLAG_NAME = 'navigation.solutionNavEnabled';
-export const DEFAULT_SOLUTION_NAV_UI_SETTING_ID = 'solutionNav:default';
+export const ENABLE_SOLUTION_NAV_UI_SETTING_ID = 'solutionNav:enable';
diff --git a/src/plugins/navigation/common/index.ts b/src/plugins/navigation/common/index.ts
index 94e93f76ba1e1..e50192be5c7a7 100644
--- a/src/plugins/navigation/common/index.ts
+++ b/src/plugins/navigation/common/index.ts
@@ -6,8 +6,4 @@
* Side Public License, v 1.
*/
-export {
- DEFAULT_SOLUTION_NAV_UI_SETTING_ID,
- ENABLE_SOLUTION_NAV_UI_SETTING_ID,
- OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID,
-} from './constants';
+export { SOLUTION_NAV_FEATURE_FLAG_NAME, ENABLE_SOLUTION_NAV_UI_SETTING_ID } from './constants';
diff --git a/src/plugins/navigation/kibana.jsonc b/src/plugins/navigation/kibana.jsonc
index 73ab422db2fad..73db4955c6ee4 100644
--- a/src/plugins/navigation/kibana.jsonc
+++ b/src/plugins/navigation/kibana.jsonc
@@ -6,7 +6,7 @@
"id": "navigation",
"server": true,
"browser": true,
- "optionalPlugins": ["cloud","security"],
+ "optionalPlugins": ["cloud","security", "cloudExperiments"],
"requiredPlugins": ["unifiedSearch"],
"requiredBundles": []
}
diff --git a/src/plugins/navigation/public/index.ts b/src/plugins/navigation/public/index.ts
index 44a90ce5598b1..2961c09e33da8 100644
--- a/src/plugins/navigation/public/index.ts
+++ b/src/plugins/navigation/public/index.ts
@@ -19,7 +19,6 @@ export { TopNavMenu, TopNavMenuItems, TopNavMenuBadges } from './top_nav_menu';
export type {
NavigationPublicSetup as NavigationPublicPluginSetup,
NavigationPublicStart as NavigationPublicPluginStart,
- SolutionNavigationOptInStatus,
SolutionType,
} from './types';
diff --git a/src/plugins/navigation/public/plugin.test.ts b/src/plugins/navigation/public/plugin.test.ts
index bc29086fe77be..84dadf234c21b 100644
--- a/src/plugins/navigation/public/plugin.test.ts
+++ b/src/plugins/navigation/public/plugin.test.ts
@@ -6,18 +6,15 @@
* Side Public License, v 1.
*/
+import { firstValueFrom, of } from 'rxjs';
import { coreMock } from '@kbn/core/public/mocks';
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
import { securityMock } from '@kbn/security-plugin/public/mocks';
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
-import { firstValueFrom, of } from 'rxjs';
+import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks';
import type { BuildFlavor } from '@kbn/config';
import type { UserSettingsData } from '@kbn/user-profile-components';
-import {
- DEFAULT_SOLUTION_NAV_UI_SETTING_ID,
- ENABLE_SOLUTION_NAV_UI_SETTING_ID,
- OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID,
-} from '../common';
+import { ENABLE_SOLUTION_NAV_UI_SETTING_ID, SOLUTION_NAV_FEATURE_FLAG_NAME } from '../common';
import { NavigationPublicPlugin } from './plugin';
import { ConfigSchema } from './types';
import { SolutionNavUserProfileToggle } from './solution_nav_userprofile_toggle';
@@ -31,9 +28,7 @@ jest.mock('rxjs', () => {
});
const defaultConfig: ConfigSchema['solutionNavigation'] = {
- featureOn: true,
enabled: true,
- optInStatus: 'visible',
defaultSolution: 'es',
};
@@ -44,34 +39,62 @@ const setup = (
{
buildFlavor = 'traditional',
userSettings = {},
- }: { buildFlavor?: BuildFlavor; userSettings?: UserSettingsData } = {}
+ uiSettingsValues,
+ }: {
+ buildFlavor?: BuildFlavor;
+ userSettings?: UserSettingsData;
+ uiSettingsValues?: Record;
+ } = {}
) => {
- const initializerContext = coreMock.createPluginInitializerContext(
- {
- solutionNavigation: {
- ...defaultConfig,
- ...partialConfig,
- },
+ const config = {
+ solutionNavigation: {
+ ...defaultConfig,
+ ...partialConfig,
},
- { buildFlavor }
- );
+ };
+
+ const initializerContext = coreMock.createPluginInitializerContext(config, { buildFlavor });
const plugin = new NavigationPublicPlugin(initializerContext);
+ const setChromeStyle = jest.fn();
const coreStart = coreMock.createStart();
const unifiedSearch = unifiedSearchPluginMock.createStartContract();
const cloud = cloudMock.createStart();
const security = securityMock.createStart();
+ const cloudExperiments = cloudExperimentsMock.createStartMock();
+ cloudExperiments.getVariation.mockImplementation((key) => {
+ if (key === SOLUTION_NAV_FEATURE_FLAG_NAME) {
+ return Promise.resolve(partialConfig.featureOn);
+ }
+ return Promise.resolve(false);
+ });
+
security.userProfiles.userProfileLoaded$ = of(true);
security.userProfiles.userProfile$ = of({ userSettings });
const getGlobalSetting$ = jest.fn();
+ if (uiSettingsValues) {
+ getGlobalSetting$.mockImplementation((settingId: string) =>
+ of(uiSettingsValues[settingId] ?? 'unknown')
+ );
+ }
const settingsGlobalClient = {
...coreStart.settings.globalClient,
get$: getGlobalSetting$,
};
coreStart.settings.globalClient = settingsGlobalClient;
+ coreStart.chrome.setChromeStyle = setChromeStyle;
- return { plugin, coreStart, unifiedSearch, cloud, security, getGlobalSetting$ };
+ return {
+ plugin,
+ coreStart,
+ unifiedSearch,
+ cloud,
+ security,
+ cloudExperiments,
+ config,
+ setChromeStyle,
+ };
};
describe('Navigation Plugin', () => {
@@ -97,21 +120,22 @@ describe('Navigation Plugin', () => {
describe('feature flag enabled', () => {
const featureOn = true;
- it('should add the default solution navs but **not** set the active nav', () => {
- const { plugin, coreStart, unifiedSearch, cloud, getGlobalSetting$ } = setup({ featureOn });
-
+ it('should add the default solution navs but **not** set the active nav', async () => {
const uiSettingsValues: Record = {
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: false, // NOT enabled, so we should not set the active nav
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'visible',
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
};
- getGlobalSetting$.mockImplementation((settingId: string) => {
- const value = uiSettingsValues[settingId];
- return of(value);
- });
+ const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
+ {
+ featureOn,
+ },
+ { uiSettingsValues }
+ );
- plugin.start(coreStart, { unifiedSearch, cloud });
+ plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
+
+ // We need to wait for the next tick to allow the promise to fetch the feature flag to resolve
+ await new Promise((resolve) => setTimeout(resolve));
expect(coreStart.chrome.project.updateSolutionNavigations).toHaveBeenCalled();
const [arg] = coreStart.chrome.project.updateSolutionNavigations.mock.calls[0];
@@ -120,73 +144,107 @@ describe('Navigation Plugin', () => {
expect(coreStart.chrome.project.changeActiveSolutionNavigation).toHaveBeenCalledWith(null);
});
- it('should add the default solution navs **and** set the active nav', () => {
- const { plugin, coreStart, unifiedSearch, cloud, getGlobalSetting$ } = setup({ featureOn });
-
+ it('should add the default solution navs **and** set the active nav', async () => {
const uiSettingsValues: Record = {
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'visible',
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'security',
};
- getGlobalSetting$.mockImplementation((settingId: string) => {
- const value = uiSettingsValues[settingId] ?? 'unknown';
- return of(value);
- });
+ const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
+ { featureOn, defaultSolution: 'security' },
+ { uiSettingsValues }
+ );
- plugin.start(coreStart, { unifiedSearch, cloud });
+ plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
+ await new Promise((resolve) => setTimeout(resolve));
expect(coreStart.chrome.project.updateSolutionNavigations).toHaveBeenCalled();
expect(coreStart.chrome.project.changeActiveSolutionNavigation).toHaveBeenCalledWith(
- uiSettingsValues[DEFAULT_SOLUTION_NAV_UI_SETTING_ID],
+ 'security',
{ onlyIfNotSet: true }
);
});
- it('if not "visible", should not set the active nav', () => {
- const { plugin, coreStart, unifiedSearch, cloud, getGlobalSetting$ } = setup({ featureOn });
-
+ it('should add the opt in/out toggle in the user menu', async () => {
const uiSettingsValues: Record = {
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'hidden',
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'security',
};
- getGlobalSetting$.mockImplementation((settingId: string) => {
- const value = uiSettingsValues[settingId] ?? 'unknown';
- return of(value);
- });
+ const { plugin, coreStart, unifiedSearch, cloud, security, cloudExperiments } = setup(
+ {
+ featureOn,
+ },
+ { uiSettingsValues }
+ );
- plugin.start(coreStart, { unifiedSearch, cloud });
+ plugin.start(coreStart, { unifiedSearch, cloud, security, cloudExperiments });
+ await new Promise((resolve) => setTimeout(resolve));
- expect(coreStart.chrome.project.updateSolutionNavigations).toHaveBeenCalled();
- expect(coreStart.chrome.project.changeActiveSolutionNavigation).toHaveBeenCalledWith(null, {
- onlyIfNotSet: true,
- });
+ expect(security.navControlService.addUserMenuLinks).toHaveBeenCalled();
+ const [menuLink] = security.navControlService.addUserMenuLinks.mock.calls[0][0];
+ expect((menuLink.content as any)?.type).toBe(SolutionNavUserProfileToggle);
});
- it('should add the opt in/out toggle in the user menu', () => {
- const { plugin, coreStart, unifiedSearch, cloud, security, getGlobalSetting$ } = setup({
- featureOn,
+ describe('set Chrome style', () => {
+ it('should set the Chrome style to "classic" when the feature is not enabled', async () => {
+ const uiSettingsValues: Record = {
+ [ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
+ };
+
+ const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
+ { featureOn: false }, // feature not enabled
+ { uiSettingsValues }
+ );
+
+ plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
+ await new Promise((resolve) => setTimeout(resolve));
+ expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
});
- const uiSettingsValues: Record = {
- [ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'visible',
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
- };
+ it('should set the Chrome style to "classic" when the feature is enabled BUT globalSettings is disabled', async () => {
+ const uiSettingsValues: Record = {
+ [ENABLE_SOLUTION_NAV_UI_SETTING_ID]: false, // Global setting disabled
+ };
- getGlobalSetting$.mockImplementation((settingId: string) => {
- const value = uiSettingsValues[settingId] ?? 'unknown';
- return of(value);
+ const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
+ { featureOn: true }, // feature enabled
+ { uiSettingsValues }
+ );
+
+ plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
+ await new Promise((resolve) => setTimeout(resolve));
+ expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('classic');
});
- plugin.start(coreStart, { unifiedSearch, cloud, security });
+ it('should NOT set the Chrome style when the feature is enabled, globalSettings is enabled BUT on serverless', async () => {
+ const uiSettingsValues: Record = {
+ [ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true, // Global setting enabled
+ };
- expect(security.navControlService.addUserMenuLinks).toHaveBeenCalled();
- const [menuLink] = security.navControlService.addUserMenuLinks.mock.calls[0][0];
- expect((menuLink.content as any)?.type).toBe(SolutionNavUserProfileToggle);
+ const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
+ { featureOn: true }, // feature enabled
+ { uiSettingsValues, buildFlavor: 'serverless' }
+ );
+
+ plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
+ await new Promise((resolve) => setTimeout(resolve));
+ expect(coreStart.chrome.setChromeStyle).not.toHaveBeenCalled();
+ });
+
+ it('should set the Chrome style to "project" when the feature is enabled', async () => {
+ const uiSettingsValues: Record = {
+ [ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
+ };
+
+ const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
+ { featureOn: true },
+ { uiSettingsValues }
+ );
+
+ plugin.start(coreStart, { unifiedSearch, cloud, cloudExperiments });
+ await new Promise((resolve) => setTimeout(resolve));
+ expect(coreStart.chrome.setChromeStyle).toHaveBeenCalledWith('project');
+ });
});
describe('isSolutionNavEnabled$', () => {
@@ -195,8 +253,6 @@ describe('Navigation Plugin', () => {
[
{
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'visible',
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
},
'should be enabled',
true,
@@ -204,17 +260,6 @@ describe('Navigation Plugin', () => {
[
{
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: false, // feature not enabled
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'visible',
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
- },
- 'should not be enabled',
- false,
- ],
- [
- {
- [ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'hidden', // not visible
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
},
'should not be enabled',
false,
@@ -223,19 +268,26 @@ describe('Navigation Plugin', () => {
testCases.forEach(([uiSettingsValues, description, expected]) => {
it(description, async () => {
- const { plugin, coreStart, unifiedSearch, cloud, getGlobalSetting$ } = setup(
+ const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
{
featureOn,
},
- { userSettings: { solutionNavOptOut: undefined } } // user has not opted in or out
+ {
+ userSettings: {
+ // user has not opted in or out
+ solutionNavOptOut: undefined,
+ },
+ uiSettingsValues,
+ }
);
- getGlobalSetting$.mockImplementation((settingId: string) => {
- const value = uiSettingsValues[settingId] ?? 'unknown';
- return of(value);
+ const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
+ unifiedSearch,
+ cloud,
+ cloudExperiments,
});
+ await new Promise((resolve) => setTimeout(resolve));
- const { isSolutionNavEnabled$ } = plugin.start(coreStart, { unifiedSearch, cloud });
expect(await firstValueFrom(isSolutionNavEnabled$)).toBe(expected);
});
});
@@ -246,8 +298,6 @@ describe('Navigation Plugin', () => {
[
{
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'visible',
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
},
'should be enabled',
true,
@@ -255,42 +305,35 @@ describe('Navigation Plugin', () => {
[
{
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: false, // feature not enabled
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'visible',
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
},
'should not be enabled',
false,
],
- [
- {
- [ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'hidden', // not visible
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
- },
- 'should be enabled',
- true,
- ],
];
testCases.forEach(([uiSettingsValues, description, expected]) => {
it(description, async () => {
- const { plugin, coreStart, unifiedSearch, cloud, security, getGlobalSetting$ } = setup(
+ const { plugin, coreStart, unifiedSearch, cloud, security, cloudExperiments } = setup(
{
featureOn,
},
- { userSettings: { solutionNavOptOut: false } } // user has opted in
+ {
+ userSettings: {
+ // user has opted in
+ solutionNavOptOut: false,
+ },
+ uiSettingsValues,
+ }
);
- getGlobalSetting$.mockImplementation((settingId: string) => {
- const value = uiSettingsValues[settingId] ?? 'unknown';
- return of(value);
- });
-
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
security,
unifiedSearch,
cloud,
+ cloudExperiments,
});
+ await new Promise((resolve) => setTimeout(resolve));
+
expect(await firstValueFrom(isSolutionNavEnabled$)).toBe(expected);
});
});
@@ -301,8 +344,6 @@ describe('Navigation Plugin', () => {
[
{
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'visible',
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
},
'should not be enabled',
false,
@@ -310,17 +351,6 @@ describe('Navigation Plugin', () => {
[
{
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: false, // feature not enabled
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'visible',
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
- },
- 'should not be enabled',
- false,
- ],
- [
- {
- [ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true,
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'hidden',
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
},
'should not be enabled',
false,
@@ -329,45 +359,43 @@ describe('Navigation Plugin', () => {
testCases.forEach(([uiSettingsValues, description, expected]) => {
it(description, async () => {
- const { plugin, coreStart, unifiedSearch, cloud, security, getGlobalSetting$ } = setup(
+ const { plugin, coreStart, unifiedSearch, cloud, security, cloudExperiments } = setup(
{
featureOn,
},
- { userSettings: { solutionNavOptOut: true } } // user has opted out
+ { userSettings: { solutionNavOptOut: true }, uiSettingsValues } // user has opted out
);
- getGlobalSetting$.mockImplementation((settingId: string) => {
- const value = uiSettingsValues[settingId] ?? 'unknown';
- return of(value);
- });
-
const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
security,
unifiedSearch,
cloud,
+ cloudExperiments,
});
+ await new Promise((resolve) => setTimeout(resolve));
+
expect(await firstValueFrom(isSolutionNavEnabled$)).toBe(expected);
});
});
});
it('on serverless should flag must be disabled', async () => {
- const { plugin, coreStart, unifiedSearch, cloud, getGlobalSetting$ } = setup(
- { featureOn },
- { buildFlavor: 'serverless' }
- );
const uiSettingsValues: Record = {
[ENABLE_SOLUTION_NAV_UI_SETTING_ID]: true, // enabled, but we are on serverless
- [OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID]: 'visible', // should not matter
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: 'es',
};
- getGlobalSetting$.mockImplementation((settingId: string) => {
- const value = uiSettingsValues[settingId] ?? 'unknown';
- return of(value);
+ const { plugin, coreStart, unifiedSearch, cloud, cloudExperiments } = setup(
+ { featureOn },
+ { buildFlavor: 'serverless', uiSettingsValues }
+ );
+
+ const { isSolutionNavEnabled$ } = plugin.start(coreStart, {
+ unifiedSearch,
+ cloud,
+ cloudExperiments,
});
+ await new Promise((resolve) => setTimeout(resolve));
- const { isSolutionNavEnabled$ } = plugin.start(coreStart, { unifiedSearch, cloud });
const isEnabled = await firstValueFrom(isSolutionNavEnabled$);
expect(isEnabled).toBe(false);
});
diff --git a/src/plugins/navigation/public/plugin.tsx b/src/plugins/navigation/public/plugin.tsx
index 623871dfdcab4..b72c3db318d20 100644
--- a/src/plugins/navigation/public/plugin.tsx
+++ b/src/plugins/navigation/public/plugin.tsx
@@ -10,12 +10,15 @@ import {
combineLatest,
debounceTime,
distinctUntilChanged,
+ from,
map,
Observable,
of,
ReplaySubject,
+ shareReplay,
skipWhile,
switchMap,
+ take,
takeUntil,
} from 'rxjs';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
@@ -31,11 +34,7 @@ import { definition as obltDefinition } from '@kbn/solution-nav-oblt';
import { definition as analyticsDefinition } from '@kbn/solution-nav-analytics';
import type { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
import { UserProfileData } from '@kbn/user-profile-components';
-import {
- ENABLE_SOLUTION_NAV_UI_SETTING_ID,
- OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID,
- DEFAULT_SOLUTION_NAV_UI_SETTING_ID,
-} from '../common';
+import { ENABLE_SOLUTION_NAV_UI_SETTING_ID, SOLUTION_NAV_FEATURE_FLAG_NAME } from '../common';
import type {
NavigationPublicSetup,
NavigationPublicStart,
@@ -43,7 +42,6 @@ import type {
NavigationPublicStartDependencies,
ConfigSchema,
SolutionNavigation,
- SolutionNavigationOptInStatus,
SolutionType,
} from './types';
import { TopNavMenuExtensionsRegistry, createTopNav } from './top_nav_menu';
@@ -88,7 +86,7 @@ export class NavigationPublicPlugin
this.coreStart = core;
this.depsStart = depsStart;
- const { unifiedSearch, cloud, security } = depsStart;
+ const { unifiedSearch, cloud, security, cloudExperiments } = depsStart;
const extensions = this.topNavMenuExtensionsRegistry.getAll();
const chrome = core.chrome as InternalChromeStart;
@@ -129,39 +127,65 @@ export class NavigationPublicPlugin
const config = this.initializerContext.config.get();
const {
- solutionNavigation: { featureOn: isSolutionNavigationFeatureOn },
+ solutionNavigation: { defaultSolution },
} = config;
const onCloud = cloud !== undefined; // The new side nav will initially only be available to cloud users
const isServerless = this.initializerContext.env.packageInfo.buildFlavor === 'serverless';
- const isSolutionNavEnabled = isSolutionNavigationFeatureOn && onCloud && !isServerless;
- this.isSolutionNavEnabled$ = of(isSolutionNavEnabled);
- if (isSolutionNavEnabled) {
- chrome.project.setCloudUrls(cloud);
- this.addDefaultSolutionNavigation({ chrome });
+ let isSolutionNavExperiementEnabled$ = of(false);
+ this.isSolutionNavEnabled$ = of(false);
+
+ if (cloudExperiments) {
+ isSolutionNavExperiementEnabled$ =
+ !onCloud || isServerless
+ ? of(false)
+ : from(cloudExperiments.getVariation(SOLUTION_NAV_FEATURE_FLAG_NAME, false)).pipe(
+ shareReplay(1)
+ );
- this.isSolutionNavEnabled$ = combineLatest([
- core.settings.globalClient.get$(ENABLE_SOLUTION_NAV_UI_SETTING_ID),
- core.settings.globalClient.get$(
- OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID
- ),
- this.userProfileOptOut$,
- ]).pipe(
- takeUntil(this.stop$),
- debounceTime(10),
- map(([enabled, status, userOptedOut]) => {
- if (!enabled || userOptedOut === true) return false;
- if (status === 'hidden' && userOptedOut === undefined) return false;
- return true;
+ this.isSolutionNavEnabled$ = isSolutionNavExperiementEnabled$.pipe(
+ switchMap((isFeatureEnabled) => {
+ return !isFeatureEnabled
+ ? of(false)
+ : combineLatest([
+ core.settings.globalClient.get$(ENABLE_SOLUTION_NAV_UI_SETTING_ID),
+ this.userProfileOptOut$,
+ ]).pipe(
+ takeUntil(this.stop$),
+ debounceTime(10),
+ map(([enabled, userOptedOut]) => {
+ if (!enabled || userOptedOut === true) return false;
+ return true;
+ })
+ );
})
);
-
- this.susbcribeToSolutionNavUiSettings({ core, security });
- } else if (!isServerless) {
- chrome.setChromeStyle('classic');
}
+ this.isSolutionNavEnabled$
+ .pipe(takeUntil(this.stop$), distinctUntilChanged())
+ .subscribe((isSolutionNavEnabled) => {
+ if (isServerless) return; // Serverless already controls the chrome style
+
+ chrome.setChromeStyle(isSolutionNavEnabled ? 'project' : 'classic');
+ });
+
+ // Initialize the solution navigation if it is enabled
+ isSolutionNavExperiementEnabled$.pipe(take(1)).subscribe((isFeatureEnabled) => {
+ if (!isFeatureEnabled) return;
+
+ chrome.project.setCloudUrls(cloud!);
+ this.addDefaultSolutionNavigation({ chrome });
+ this.susbcribeToSolutionNavUiSettings({ core, security, defaultSolution });
+ });
+
+ // Keep track of the solution navigation enabled state
+ let isSolutionNavEnabled = false;
+ this.isSolutionNavEnabled$.pipe(takeUntil(this.stop$)).subscribe((_isSolutionNavEnabled) => {
+ isSolutionNavEnabled = _isSolutionNavEnabled;
+ });
+
return {
ui: {
TopNavMenu: createTopNav(unifiedSearch, extensions),
@@ -190,25 +214,23 @@ export class NavigationPublicPlugin
private susbcribeToSolutionNavUiSettings({
core,
security,
+ defaultSolution,
}: {
core: CoreStart;
+ defaultSolution: SolutionType;
security?: SecurityPluginStart;
}) {
const chrome = core.chrome as InternalChromeStart;
combineLatest([
core.settings.globalClient.get$(ENABLE_SOLUTION_NAV_UI_SETTING_ID),
- core.settings.globalClient.get$(
- OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID
- ),
- core.settings.globalClient.get$(DEFAULT_SOLUTION_NAV_UI_SETTING_ID),
this.userProfileOptOut$,
])
.pipe(takeUntil(this.stop$), debounceTime(10))
- .subscribe(([enabled, status, defaultSolution, userOptedOut]) => {
+ .subscribe(([enabled, userOptedOut]) => {
if (enabled) {
// Add menu item in the user profile menu to opt in/out of the new navigation
- this.addOptInOutUserProfile({ core, security, optInStatusSetting: status, userOptedOut });
+ this.addOptInOutUserProfile({ core, security, userOptedOut });
} else {
// TODO. Remove the user profile menu item if the feature is disabled.
// But first let's wait as maybe there will be a page refresh when opting out.
@@ -218,17 +240,7 @@ export class NavigationPublicPlugin
chrome.project.changeActiveSolutionNavigation(null);
chrome.setChromeStyle('classic');
} else {
- const changeToSolutionNav =
- status === 'visible' || (status === 'hidden' && userOptedOut === false);
-
- if (!changeToSolutionNav) {
- chrome.setChromeStyle('classic');
- }
-
- chrome.project.changeActiveSolutionNavigation(
- changeToSolutionNav ? defaultSolution : null,
- { onlyIfNotSet: true }
- );
+ chrome.project.changeActiveSolutionNavigation(defaultSolution, { onlyIfNotSet: true });
}
});
}
@@ -290,29 +302,21 @@ export class NavigationPublicPlugin
sideNavComponent: this.getSideNavComponent({ dataTestSubj: 'analyticsSideNav' }),
},
};
-
chrome.project.updateSolutionNavigations(solutionNavs, true);
}
private addOptInOutUserProfile({
core,
security,
- optInStatusSetting,
userOptedOut,
}: {
core: CoreStart;
userOptedOut?: boolean;
- optInStatusSetting?: SolutionNavigationOptInStatus;
security?: SecurityPluginStart;
}) {
if (!security || this.userProfileMenuItemAdded) return;
- let defaultOptOutValue = userOptedOut !== undefined ? userOptedOut : DEFAULT_OPT_OUT_NEW_NAV;
- if (optInStatusSetting === 'visible' && userOptedOut === undefined) {
- defaultOptOutValue = false;
- } else if (optInStatusSetting === 'hidden' && userOptedOut === undefined) {
- defaultOptOutValue = true;
- }
+ const defaultOptOutValue = userOptedOut !== undefined ? userOptedOut : DEFAULT_OPT_OUT_NEW_NAV;
const menuLink: UserMenuLink = {
content: (
diff --git a/src/plugins/navigation/public/types.ts b/src/plugins/navigation/public/types.ts
index e9a7c298c1a97..90eb5f8c34c13 100644
--- a/src/plugins/navigation/public/types.ts
+++ b/src/plugins/navigation/public/types.ts
@@ -12,6 +12,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
import type { SolutionNavigationDefinition } from '@kbn/core-chrome-browser';
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public';
+import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
import { TopNavMenuProps, TopNavMenuExtensionsRegistrySetup, createTopNav } from './top_nav_menu';
import type { RegisteredTopNavMenuData } from './top_nav_menu/top_nav_menu_data';
@@ -46,17 +47,14 @@ export interface NavigationPublicStartDependencies {
unifiedSearch: UnifiedSearchPublicPluginStart;
cloud?: CloudStart;
security?: SecurityPluginStart;
+ cloudExperiments?: CloudExperimentsPluginStart;
}
-export type SolutionNavigationOptInStatus = 'visible' | 'hidden' | 'ask';
-
-export type SolutionType = 'es' | 'oblt' | 'security';
+export type SolutionType = 'es' | 'oblt' | 'security' | 'analytics';
export interface ConfigSchema {
solutionNavigation: {
- featureOn: boolean;
enabled: boolean;
- optInStatus: SolutionNavigationOptInStatus;
- defaultSolution: SolutionType | 'ask';
+ defaultSolution: SolutionType;
};
}
diff --git a/src/plugins/navigation/server/config.ts b/src/plugins/navigation/server/config.ts
index abf62b0da37ed..5283da958c677 100644
--- a/src/plugins/navigation/server/config.ts
+++ b/src/plugins/navigation/server/config.ts
@@ -11,18 +11,13 @@ import type { PluginConfigDescriptor } from '@kbn/core-plugins-server';
const configSchema = schema.object({
solutionNavigation: schema.object({
- featureOn: schema.boolean({ defaultValue: false }),
enabled: schema.boolean({ defaultValue: false }),
- optInStatus: schema.oneOf(
- [schema.literal('visible'), schema.literal('hidden'), schema.literal('ask')],
- { defaultValue: 'ask' }
- ),
defaultSolution: schema.oneOf(
[
- schema.literal('ask'),
schema.literal('es'),
schema.literal('oblt'),
schema.literal('security'),
+ schema.literal('analytics'),
],
{ defaultValue: 'es' }
),
diff --git a/src/plugins/navigation/server/plugin.ts b/src/plugins/navigation/server/plugin.ts
index 2220d19cc6768..e5d83b915b897 100644
--- a/src/plugins/navigation/server/plugin.ts
+++ b/src/plugins/navigation/server/plugin.ts
@@ -7,6 +7,7 @@
*/
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server';
import type { UiSettingsParams } from '@kbn/core/types';
+import { SOLUTION_NAV_FEATURE_FLAG_NAME } from '../common';
import type { NavigationConfig } from './config';
import type {
@@ -28,25 +29,28 @@ export class NavigationServerPlugin
{
constructor(private initializerContext: PluginInitializerContext) {}
- setup(core: CoreSetup, plugins: NavigationServerSetupDependencies) {
- if (!this.isServerless()) {
+ setup(
+ core: CoreSetup,
+ plugins: NavigationServerSetupDependencies
+ ) {
+ if (plugins.cloud?.isCloudEnabled && !this.isServerless()) {
const config = this.initializerContext.config.get();
- if (config.solutionNavigation.featureOn) {
- core.uiSettings.registerGlobal(getUiSettings(config));
- }
+ core.getStartServices().then(([coreStart, deps]) => {
+ deps.cloudExperiments?.getVariation(SOLUTION_NAV_FEATURE_FLAG_NAME, false).then((value) => {
+ if (value) {
+ core.uiSettings.registerGlobal(getUiSettings(config));
+ } else {
+ this.removeUiSettings(coreStart, getUiSettings(config));
+ }
+ });
+ });
}
return {};
}
start(core: CoreStart, plugins: NavigationServerStartDependencies) {
- const config = this.initializerContext.config.get();
-
- if (!Boolean(config.solutionNavigation.featureOn)) {
- this.removeUiSettings(core, getUiSettings(config));
- }
-
return {};
}
diff --git a/src/plugins/navigation/server/types.ts b/src/plugins/navigation/server/types.ts
index 920e1e5b8cd52..e6fadbc6ad932 100644
--- a/src/plugins/navigation/server/types.ts
+++ b/src/plugins/navigation/server/types.ts
@@ -5,6 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
+import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface NavigationServerSetup {}
@@ -12,8 +14,11 @@ export interface NavigationServerSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface NavigationServerStart {}
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface NavigationServerSetupDependencies {}
+export interface NavigationServerSetupDependencies {
+ cloud?: CloudSetup;
+}
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface NavigationServerStartDependencies {}
+export interface NavigationServerStartDependencies {
+ cloudExperiments?: CloudExperimentsPluginStart;
+ cloud?: CloudStart;
+}
diff --git a/src/plugins/navigation/server/ui_settings.ts b/src/plugins/navigation/server/ui_settings.ts
index 3d40ace802375..efc744ae0dd79 100644
--- a/src/plugins/navigation/server/ui_settings.ts
+++ b/src/plugins/navigation/server/ui_settings.ts
@@ -10,40 +10,9 @@ import { schema } from '@kbn/config-schema';
import { UiSettingsParams } from '@kbn/core/types';
import { i18n } from '@kbn/i18n';
-import {
- ENABLE_SOLUTION_NAV_UI_SETTING_ID,
- DEFAULT_SOLUTION_NAV_UI_SETTING_ID,
- OPT_IN_STATUS_SOLUTION_NAV_UI_SETTING_ID,
-} from '../common/constants';
+import { ENABLE_SOLUTION_NAV_UI_SETTING_ID } from '../common/constants';
import { NavigationConfig } from './config';
-const optInStatusOptionLabels = {
- visible: i18n.translate('navigation.advancedSettings.optInVisibleStatus', {
- defaultMessage: 'Visible',
- }),
- hidden: i18n.translate('navigation.advancedSettings.optInHiddenStatus', {
- defaultMessage: 'Hidden',
- }),
- ask: i18n.translate('navigation.advancedSettings.optInAskStatus', {
- defaultMessage: 'Ask',
- }),
-};
-
-const solutionsOptionLabels = {
- ask: i18n.translate('navigation.advancedSettings.askUserWhichSolution', {
- defaultMessage: 'Ask user to choose a solution',
- }),
- es: i18n.translate('navigation.advancedSettings.searchSolution', {
- defaultMessage: 'Search',
- }),
- oblt: i18n.translate('navigation.advancedSettings.observabilitySolution', {
- defaultMessage: 'Observability',
- }),
- security: i18n.translate('navigation.advancedSettings.securitySolution', {
- defaultMessage: 'Security',
- }),
-};
-
const categoryLabel = i18n.translate('navigation.uiSettings.categoryLabel', {
defaultMessage: 'Technical preview',
});
@@ -59,52 +28,11 @@ export const getUiSettings = (config: NavigationConfig): Record
- {visible}: The new navigation is visible immediately to all user. They will be able to opt-out from their user profile.
- {hidden}: The new navigation is hidden by default. Users can opt-in from their user profile. No banners are shown.
- {ask}: Show a banner to the users inviting them to try the new navigation experience.
- `,
- values: {
- visible: optInStatusOptionLabels.visible,
- hidden: optInStatusOptionLabels.hidden,
- ask: optInStatusOptionLabels.ask,
- },
- }),
- name: i18n.translate('navigation.uiSettings.optInStatusSolutionNav.name', {
- defaultMessage: 'Opt-in behaviour',
- }),
- type: 'select',
- schema: schema.string(),
- value: config.solutionNavigation.optInStatus,
- options: ['visible', 'hidden', 'ask'],
- optionLabels: optInStatusOptionLabels,
- order: 2,
- },
- [DEFAULT_SOLUTION_NAV_UI_SETTING_ID]: {
- category: [categoryLabel],
- description: i18n.translate('navigation.uiSettings.defaultSolutionNav.description', {
- defaultMessage:
- 'The default solution to display to the users once they opt-in to the new navigation.',
- }),
- name: i18n.translate('navigation.uiSettings.defaultSolutionNav.name', {
- defaultMessage: 'Default solution',
- }),
- type: 'select',
- schema: schema.string(),
- value: config.solutionNavigation.defaultSolution,
- options: ['ask', 'es', 'oblt', 'security'],
- optionLabels: solutionsOptionLabels,
- order: 2,
- },
};
};
diff --git a/src/plugins/navigation/tsconfig.json b/src/plugins/navigation/tsconfig.json
index ecf3aba49c9ce..4a9c4832ea42d 100644
--- a/src/plugins/navigation/tsconfig.json
+++ b/src/plugins/navigation/tsconfig.json
@@ -31,6 +31,7 @@
"@kbn/security-plugin",
"@kbn/user-profile-components",
"@kbn/core-lifecycle-browser",
+ "@kbn/cloud-experiments-plugin",
],
"exclude": [
"target/**/*",
diff --git a/test/functional/apps/navigation/config.ts b/test/functional/apps/navigation/config.ts
deleted file mode 100644
index 19df1f83ec844..0000000000000
--- a/test/functional/apps/navigation/config.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { FtrConfigProviderContext } from '@kbn/test';
-
-export default async function ({ readConfigFile }: FtrConfigProviderContext) {
- const functionalConfig = await readConfigFile(require.resolve('../../config.base.js'));
-
- return {
- ...functionalConfig.getAll(),
- testFiles: [require.resolve('.')],
- kbnTestServer: {
- ...functionalConfig.get('kbnTestServer'),
- serverArgs: [
- ...functionalConfig.get('kbnTestServer.serverArgs'),
- '--navigation.solutionNavigation.featureOn=true',
- '--navigation.solutionNavigation.enabled=true',
- '--navigation.solutionNavigation.optInStatus=visible',
- '--navigation.solutionNavigation.defaultSolution=es',
- // Note: the base64 string in the cloud.id config contains the ES endpoint required in the functional tests
- '--xpack.cloud.id=ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM=',
- '--xpack.cloud.base_url=https://cloud.elastic.co',
- '--xpack.cloud.deployment_url=/deployments/deploymentId',
- '--xpack.cloud.organization_url=/organization/organizationId',
- '--xpack.cloud.billing_url=/billing',
- '--xpack.cloud.profile_url=/user/userId',
- ],
- },
- };
-}
diff --git a/test/functional/apps/navigation/index.ts b/test/functional/apps/navigation/index.ts
deleted file mode 100644
index 6a05d098e794e..0000000000000
--- a/test/functional/apps/navigation/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { FtrProviderContext } from '../../ftr_provider_context';
-
-export default function ({ loadTestFile }: FtrProviderContext) {
- describe('navigation app', function () {
- loadTestFile(require.resolve('./_solution_nav_switcher'));
- });
-}
diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts
index d6277fbaf394c..22066abf17d20 100644
--- a/test/plugin_functional/test_suites/core_plugins/rendering.ts
+++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts
@@ -160,9 +160,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'monitoring.ui.enabled (boolean)',
'monitoring.ui.min_interval_seconds (number)',
'monitoring.ui.show_license_expiration (boolean)',
- 'navigation.solutionNavigation.featureOn (boolean)',
'navigation.solutionNavigation.enabled (boolean)',
- 'navigation.solutionNavigation.optInStatus (alternatives)',
'navigation.solutionNavigation.defaultSolution (alternatives)',
'newsfeed.fetchInterval (duration)',
'newsfeed.mainInterval (duration)',
diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/common/constants.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/common/constants.ts
index 2ae72dabb6484..d0e77614f4be3 100644
--- a/x-pack/plugins/cloud_integrations/cloud_experiments/common/constants.ts
+++ b/x-pack/plugins/cloud_integrations/cloud_experiments/common/constants.ts
@@ -38,6 +38,10 @@ export enum FEATURE_FLAG_NAMES {
* Options are: 'header' (the chat button appears as part of the kibana header) and 'bubble' (floating chat button at the bottom of the screen).
*/
'cloud-chat.chat-variant' = 'cloud-chat.chat-variant',
+ /**
+ * Used to enable the new stack navigation around solutions during the rollout period.
+ */
+ 'navigation.solutionNavEnabled' = 'navigation.solutionNavEnabled',
}
/**
diff --git a/x-pack/test/functional/apps/navigation/config.ts b/x-pack/test/functional/apps/navigation/config.ts
index 00b98c1db316a..b90946c8511fa 100644
--- a/x-pack/test/functional/apps/navigation/config.ts
+++ b/x-pack/test/functional/apps/navigation/config.ts
@@ -36,9 +36,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
...kibanaFunctionalConfig.get('kbnTestServer'),
serverArgs: [
...kibanaFunctionalConfig.get('kbnTestServer.serverArgs'),
- '--navigation.solutionNavigation.featureOn=true',
+ '--xpack.cloud_integrations.experiments.enabled=true',
+ '--xpack.cloud_integrations.experiments.launch_darkly.sdk_key=a_string',
+ '--xpack.cloud_integrations.experiments.launch_darkly.client_id=a_string',
+ '--xpack.cloud_integrations.experiments.flag_overrides.navigation.solutionNavEnabled=true',
'--navigation.solutionNavigation.enabled=true',
- '--navigation.solutionNavigation.optInStatus=visible',
'--navigation.solutionNavigation.defaultSolution=es',
// Note: the base64 string in the cloud.id config contains the ES endpoint required in the functional tests
'--xpack.cloud.id=ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM=',
diff --git a/x-pack/test/functional/apps/navigation/tests/index.ts b/x-pack/test/functional/apps/navigation/tests/index.ts
index 3f0f6acaf2231..f2a1fbc926841 100644
--- a/x-pack/test/functional/apps/navigation/tests/index.ts
+++ b/x-pack/test/functional/apps/navigation/tests/index.ts
@@ -9,6 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('navigation - functional tests', function () {
+ loadTestFile(require.resolve('./solution_nav_switcher'));
loadTestFile(require.resolve('./user_optin_optout'));
});
}
diff --git a/test/functional/apps/navigation/_solution_nav_switcher.ts b/x-pack/test/functional/apps/navigation/tests/solution_nav_switcher.ts
similarity index 84%
rename from test/functional/apps/navigation/_solution_nav_switcher.ts
rename to x-pack/test/functional/apps/navigation/tests/solution_nav_switcher.ts
index 5cf764aa3f413..b9d9803d043c0 100644
--- a/test/functional/apps/navigation/_solution_nav_switcher.ts
+++ b/x-pack/test/functional/apps/navigation/tests/solution_nav_switcher.ts
@@ -1,11 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
*/
-import { FtrProviderContext } from '../../ftr_provider_context';
+
+import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'header', 'discover', 'dashboard']);
From 4ef6fcb371cde0669052773e3941ce6900a2dd39 Mon Sep 17 00:00:00 2001
From: Justin Kambic
Date: Wed, 10 Apr 2024 08:53:29 -0400
Subject: [PATCH 09/55] [Observability Onboading] Export additional types for
usage with Package List Grid (#180416)
## Summary
Related to https://github.com/elastic/kibana/pull/179573, exporting
these additional types make downstream usage of the `PackageListGrid`
component and `useAvailablePackages` hook easier.
Co-authored-by: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
---
.../epm/components/package_list_grid/index.stories.tsx | 4 ++--
.../sections/epm/components/package_list_grid/index.tsx | 4 ++--
.../epm/components/utils/promote_featured_integrations.ts | 2 +-
.../epm/screens/home/hooks/use_available_packages.tsx | 2 ++
x-pack/plugins/fleet/public/index.ts | 4 ++++
5 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.stories.tsx
index 0cfb804980008..8639d18b1278b 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.stories.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.stories.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { action } from '@storybook/addon-actions';
-import type { Props } from '.';
+import type { PackageListGridProps } from '.';
import { PackageListGrid } from '.';
export default {
@@ -18,7 +18,7 @@ export default {
};
type Args = Pick<
- Props,
+ PackageListGridProps,
'title' | 'isLoading' | 'showMissingIntegrationMessage' | 'showControls' | 'showSearchTools'
>;
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx
index 0ff1a708dabc2..8a6761d48f9b1 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx
@@ -48,7 +48,7 @@ const StickySidebar = styled(EuiFlexItem)`
top: 120px;
`;
-export interface Props {
+export interface PackageListGridProps {
isLoading?: boolean;
controls?: ReactNode | ReactNode[];
list: IntegrationCardItem[];
@@ -71,7 +71,7 @@ export interface Props {
showSearchTools?: boolean;
}
-export const PackageListGrid: FunctionComponent = ({
+export const PackageListGrid: FunctionComponent = ({
isLoading,
controls,
title,
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/utils/promote_featured_integrations.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/utils/promote_featured_integrations.ts
index 675d3691c1fd5..07d41ea431dab 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/utils/promote_featured_integrations.ts
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/utils/promote_featured_integrations.ts
@@ -8,7 +8,7 @@ import partition from 'lodash/partition';
import { FEATURED_INTEGRATIONS_BY_CATEGORY } from '@kbn/custom-integrations-plugin/common';
-import type { Props as PackageListGridProps } from '../package_list_grid';
+import type { PackageListGridProps } from '../package_list_grid';
type Category = PackageListGridProps['selectedCategory'];
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx
index 9aa242f8575b3..c7b1f936e2424 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx
@@ -103,6 +103,8 @@ const packageListToIntegrationsList = (packages: PackageList): PackageList => {
}, []);
};
+export type AvailablePackagesHookType = typeof useAvailablePackages;
+
export const useAvailablePackages = ({
prereleaseIntegrationsEnabled,
}: {
diff --git a/x-pack/plugins/fleet/public/index.ts b/x-pack/plugins/fleet/public/index.ts
index 003e3de3f35dd..685a50230912b 100644
--- a/x-pack/plugins/fleet/public/index.ts
+++ b/x-pack/plugins/fleet/public/index.ts
@@ -65,6 +65,10 @@ export type { PackagePolicyEditorDatastreamMappingsProps } from './applications/
export type { DynamicPagePathValues } from './constants';
+export type { PackageListGridProps } from './applications/integrations/sections/epm/components/package_list_grid';
+export type { AvailablePackagesHookType } from './applications/integrations/sections/epm/screens/home/hooks/use_available_packages';
+export type { IntegrationCardItem } from './applications/integrations/sections/epm/screens/home';
+
export const PackageList = () => {
return import('./applications/integrations/sections/epm/components/package_list_grid');
};
From d42c4bb6b31d39543de40178c914cdbccd8ddd28 Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Wed, 10 Apr 2024 09:15:42 -0400
Subject: [PATCH 10/55] [Security Solution][Endpoint] Fix Sentinelone
`isolate`/`release` new action completion logic so that its gated by feature
flag (#180390)
## Summary
- Updates both `release` and `isolate` actions for SentinelOne so that
the new (`8.14.0`) approach for completing the response actions is gated
by the `responseActionsSentinelOneV2Enabled` feature flag
---
.../sentinel_one_actions_client.test.ts | 124 +++++++++++++++++-
.../sentinel_one_actions_client.ts | 26 ++++
2 files changed, 148 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts
index a0a585b12f7ec..9d96ca462c53d 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts
@@ -13,6 +13,7 @@ import { ResponseActionsNotSupportedError } from '../errors';
import type { SentinelOneActionsClientOptionsMock } from './mocks';
import { sentinelOneMock } from './mocks';
import {
+ ENDPOINT_ACTION_RESPONSES_INDEX,
ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN,
ENDPOINT_ACTIONS_INDEX,
} from '../../../../../../common/endpoint/constants';
@@ -99,7 +100,67 @@ describe('SentinelOneActionsClient class', () => {
});
});
- it('should write action request and response to endpoint indexes', async () => {
+ it('should write action request and response to endpoint indexes when `responseActionsSentinelOneV2Enabled` FF is Disabled', async () => {
+ await s1ActionsClient.isolate(createS1IsolationOptions());
+
+ expect(classConstructorOptions.esClient.index).toHaveBeenCalledTimes(2);
+ expect(classConstructorOptions.esClient.index).toHaveBeenNthCalledWith(
+ 1,
+ {
+ document: {
+ '@timestamp': expect.any(String),
+ EndpointActions: {
+ action_id: expect.any(String),
+ data: {
+ command: 'isolate',
+ comment: 'test comment',
+ parameters: undefined,
+ hosts: {
+ '1-2-3': {
+ name: 'sentinelone-1460',
+ },
+ },
+ },
+ expiration: expect.any(String),
+ input_type: 'sentinel_one',
+ type: 'INPUT_ACTION',
+ },
+ agent: { id: ['1-2-3'] },
+ user: { id: 'foo' },
+ meta: {
+ agentId: '1845174760470303882',
+ agentUUID: '1-2-3',
+ hostName: 'sentinelone-1460',
+ },
+ },
+ index: ENDPOINT_ACTIONS_INDEX,
+ refresh: 'wait_for',
+ },
+ { meta: true }
+ );
+
+ expect(classConstructorOptions.esClient.index).toHaveBeenNthCalledWith(2, {
+ document: {
+ '@timestamp': expect.any(String),
+ EndpointActions: {
+ action_id: expect.any(String),
+ data: { command: 'isolate' },
+ input_type: 'sentinel_one',
+ started_at: expect.any(String),
+ completed_at: expect.any(String),
+ },
+ agent: { id: ['1-2-3'] },
+ error: undefined,
+ },
+ index: ENDPOINT_ACTION_RESPONSES_INDEX,
+ refresh: 'wait_for',
+ });
+ });
+
+ it('should write action request (only) to endpoint indexes when `responseActionsSentinelOneV2Enabled` FF is Enabled', async () => {
+ // @ts-expect-error updating readonly attribute
+ classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneV2Enabled =
+ true;
await s1ActionsClient.isolate(createS1IsolationOptions());
expect(classConstructorOptions.esClient.index).toHaveBeenCalledTimes(1);
@@ -170,7 +231,66 @@ describe('SentinelOneActionsClient class', () => {
});
});
- it('should write action request and response to endpoint indexes', async () => {
+ it('should write action request and response to endpoint indexes when `responseActionsSentinelOneV2Enabled` is Disabled', async () => {
+ await s1ActionsClient.release(createS1IsolationOptions());
+
+ expect(classConstructorOptions.esClient.index).toHaveBeenCalledTimes(2);
+ expect(classConstructorOptions.esClient.index).toHaveBeenNthCalledWith(
+ 1,
+ {
+ document: {
+ '@timestamp': expect.any(String),
+ EndpointActions: {
+ action_id: expect.any(String),
+ data: {
+ command: 'unisolate',
+ comment: 'test comment',
+ parameters: undefined,
+ hosts: {
+ '1-2-3': {
+ name: 'sentinelone-1460',
+ },
+ },
+ },
+ expiration: expect.any(String),
+ input_type: 'sentinel_one',
+ type: 'INPUT_ACTION',
+ },
+ agent: { id: ['1-2-3'] },
+ user: { id: 'foo' },
+ meta: {
+ agentId: '1845174760470303882',
+ agentUUID: '1-2-3',
+ hostName: 'sentinelone-1460',
+ },
+ },
+ index: ENDPOINT_ACTIONS_INDEX,
+ refresh: 'wait_for',
+ },
+ { meta: true }
+ );
+ expect(classConstructorOptions.esClient.index).toHaveBeenNthCalledWith(2, {
+ document: {
+ '@timestamp': expect.any(String),
+ EndpointActions: {
+ action_id: expect.any(String),
+ data: { command: 'unisolate' },
+ input_type: 'sentinel_one',
+ started_at: expect.any(String),
+ completed_at: expect.any(String),
+ },
+ agent: { id: ['1-2-3'] },
+ error: undefined,
+ },
+ index: ENDPOINT_ACTION_RESPONSES_INDEX,
+ refresh: 'wait_for',
+ });
+ });
+
+ it('should write action request (only) to endpoint indexes when `` is Enabled', async () => {
+ // @ts-expect-error updating readonly attribute
+ classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneV2Enabled =
+ true;
await s1ActionsClient.release(createS1IsolationOptions());
expect(classConstructorOptions.esClient.index).toHaveBeenCalledTimes(1);
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts
index cd013128c42cf..f70305e4bac07 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts
@@ -252,6 +252,19 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
comment: reqIndexOptions.comment,
});
+ if (
+ !actionRequestDoc.error &&
+ !this.options.endpointService.experimentalFeatures.responseActionsSentinelOneV2Enabled
+ ) {
+ await this.writeActionResponseToEndpointIndex({
+ actionId: actionRequestDoc.EndpointActions.action_id,
+ agentId: actionRequestDoc.agent.id,
+ data: {
+ command: actionRequestDoc.EndpointActions.data.command,
+ },
+ });
+ }
+
return this.fetchActionDetails(actionRequestDoc.EndpointActions.action_id);
}
@@ -303,6 +316,19 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
comment: reqIndexOptions.comment,
});
+ if (
+ !actionRequestDoc.error &&
+ !this.options.endpointService.experimentalFeatures.responseActionsSentinelOneV2Enabled
+ ) {
+ await this.writeActionResponseToEndpointIndex({
+ actionId: actionRequestDoc.EndpointActions.action_id,
+ agentId: actionRequestDoc.agent.id,
+ data: {
+ command: actionRequestDoc.EndpointActions.data.command,
+ },
+ });
+ }
+
return this.fetchActionDetails(actionRequestDoc.EndpointActions.action_id);
}
From 83cb0e440e4082927d231b2b2488ef432cbcaf74 Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Wed, 10 Apr 2024 07:27:59 -0600
Subject: [PATCH 11/55] [embeddable rebuild]
useBatchedOptionalPublishingSubjects (#180221)
Fixes https://github.com/elastic/kibana/issues/180088
1. Changes observable pipe from `debounceTime(), skip(1)` to `skip(1),
debounceTime()`. Updates test to verify this change results in
subscription getting fired when observable.next is called before
debounceTime fires.
2. rename `useBatchedPublishingSubjects` to
`useBatchedOptionalPublishingSubjects`. Remove `useMemo` since spreading
subjects results in new array every time and useMemo does nothing.
3. Update `PresentationPanelInternal` to use
`useBatchedOptionalPublishingSubjects`
4. create new `useBatchedPublishingSubjects` that types subjects as
`PublishingSubject[]`. New
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../presentation_publishing/index.ts | 1 +
.../publishing_subject/index.ts | 5 +-
.../publishing_subject/publishing_batcher.ts | 59 +++++++++++--
.../publishing_subject.test.tsx | 88 +++++++++++++------
.../public/components/image_embeddable.tsx | 3 +-
.../presentation_panel_context_menu.tsx | 7 +-
.../presentation_panel_internal.tsx | 4 +-
7 files changed, 128 insertions(+), 39 deletions(-)
diff --git a/packages/presentation/presentation_publishing/index.ts b/packages/presentation/presentation_publishing/index.ts
index d3764da74c6d2..e4476cbd2cff9 100644
--- a/packages/presentation/presentation_publishing/index.ts
+++ b/packages/presentation/presentation_publishing/index.ts
@@ -111,6 +111,7 @@ export {
} from './interfaces/titles/publishes_panel_title';
export { initializeTitles, type SerializedTitles } from './interfaces/titles/titles_api';
export {
+ useBatchedOptionalPublishingSubjects,
useBatchedPublishingSubjects,
usePublishingSubject,
useStateFromPublishingSubject,
diff --git a/packages/presentation/presentation_publishing/publishing_subject/index.ts b/packages/presentation/presentation_publishing/publishing_subject/index.ts
index 5dbd2eb95579a..022c4170f6cde 100644
--- a/packages/presentation/presentation_publishing/publishing_subject/index.ts
+++ b/packages/presentation/presentation_publishing/publishing_subject/index.ts
@@ -6,7 +6,10 @@
* Side Public License, v 1.
*/
-export { useBatchedPublishingSubjects } from './publishing_batcher';
+export {
+ useBatchedOptionalPublishingSubjects,
+ useBatchedPublishingSubjects,
+} from './publishing_batcher';
export { useStateFromPublishingSubject, usePublishingSubject } from './publishing_subject';
export type {
PublishingSubject,
diff --git a/packages/presentation/presentation_publishing/publishing_subject/publishing_batcher.ts b/packages/presentation/presentation_publishing/publishing_subject/publishing_batcher.ts
index 4624e43c2a0d1..f04661573d918 100644
--- a/packages/presentation/presentation_publishing/publishing_subject/publishing_batcher.ts
+++ b/packages/presentation/presentation_publishing/publishing_subject/publishing_batcher.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { useEffect, useMemo, useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { combineLatest, debounceTime, skip } from 'rxjs';
import { AnyPublishingSubject, PublishingSubject, UnwrapPublishingSubjectTuple } from './types';
@@ -25,25 +25,28 @@ const hasSubjectsArrayChanged = (
/**
* Batches the latest values of multiple publishing subjects into a single object. Use this to avoid unnecessary re-renders.
- * You should avoid using this hook with subjects that your component pushes values to on user interaction, as it can cause a slight delay.
+ * Use when `subjects` may not be defined on initial component render.
+ *
* @param subjects Publishing subjects array.
* When 'subjects' is expected to change, 'subjects' must be part of component react state.
*/
-export const useBatchedPublishingSubjects = (
+export const useBatchedOptionalPublishingSubjects = <
+ SubjectsType extends [...AnyPublishingSubject[]]
+>(
...subjects: [...SubjectsType]
): UnwrapPublishingSubjectTuple => {
const isFirstRender = useRef(true);
- /**
- * memoize and deep diff subjects to avoid rebuilding the subscription when the subjects are the same.
- */
+
const previousSubjects = useRef(subjects);
- const subjectsToUse = useMemo(() => {
+ // Can not use 'useMemo' because 'subjects' gets a new reference on each call because of spread
+ const subjectsToUse = (() => {
+ // avoid rebuilding the subscription when the subjects are the same
if (!hasSubjectsArrayChanged(previousSubjects.current ?? [], subjects)) {
return previousSubjects.current;
}
previousSubjects.current = subjects;
return subjects;
- }, [subjects]);
+ })();
/**
* Set up latest published values state, initialized with the current values of the subjects.
@@ -94,6 +97,46 @@ export const useBatchedPublishingSubjects = >]
+>(
+ ...subjects: [...SubjectsType]
+): UnwrapPublishingSubjectTuple => {
+ /**
+ * Set up latest published values state, initialized with the current values of the subjects.
+ */
+ const [latestPublishedValues, setLatestPublishedValues] = useState<
+ UnwrapPublishingSubjectTuple
+ >(() => unwrapPublishingSubjectArray(subjects));
+
+ /**
+ * Subscribe to all subjects and update the latest values when any of them change.
+ */
+ useEffect(() => {
+ const subscription = combineLatest(subjects)
+ .pipe(
+ // When a new observer subscribes to a BehaviorSubject, it immediately receives the current value. Skip this emit.
+ skip(1),
+ debounceTime(0)
+ )
+ .subscribe((values) => {
+ setLatestPublishedValues(values as UnwrapPublishingSubjectTuple);
+ });
+ return () => subscription.unsubscribe();
+ // 'subjects' gets a new reference on each call because of spread
+ // Use 'useBatchedOptionalPublishingSubjects' when 'subjects' are expected to change.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return latestPublishedValues;
+};
+
const unwrapPublishingSubjectArray = (
subjects: T
): UnwrapPublishingSubjectTuple => {
diff --git a/packages/presentation/presentation_publishing/publishing_subject/publishing_subject.test.tsx b/packages/presentation/presentation_publishing/publishing_subject/publishing_subject.test.tsx
index e58ca06d54f9b..ec0d80c0dd3c9 100644
--- a/packages/presentation/presentation_publishing/publishing_subject/publishing_subject.test.tsx
+++ b/packages/presentation/presentation_publishing/publishing_subject/publishing_subject.test.tsx
@@ -11,11 +11,14 @@ import { BehaviorSubject } from 'rxjs';
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
-import { useBatchedPublishingSubjects } from './publishing_batcher';
+import {
+ useBatchedPublishingSubjects,
+ useBatchedOptionalPublishingSubjects,
+} from './publishing_batcher';
import { useStateFromPublishingSubject } from './publishing_subject';
import { PublishingSubject } from './types';
-describe('useBatchedPublishingSubjects', () => {
+describe('publishing subject', () => {
describe('render', () => {
let subject1: BehaviorSubject;
let subject2: BehaviorSubject;
@@ -56,7 +59,6 @@ describe('useBatchedPublishingSubjects', () => {
<>
{`value1: ${value1}, value2: ${value2}, value3: ${value3}, value4: ${value4}, value5: ${value5}, value6: ${value6}`}
- {renderCount}
>
);
}
@@ -72,7 +74,7 @@ describe('useBatchedPublishingSubjects', () => {
screen.getByText('value1: 1, value2: 1, value3: 1, value4: 1, value5: 1, value6: 1')
).toBeInTheDocument();
});
- expect(screen.getByTestId('renderCount')).toHaveTextContent('2');
+ expect(renderCount).toBe(2);
});
test('should batch state updates when using useBatchedPublishingSubjects', async () => {
@@ -97,7 +99,6 @@ describe('useBatchedPublishingSubjects', () => {
}}
/>
{`value1: ${value1}, value2: ${value2}, value3: ${value3}, value4: ${value4}, value5: ${value5}, value6: ${value6}`}
- {renderCount}
>
);
}
@@ -113,7 +114,48 @@ describe('useBatchedPublishingSubjects', () => {
screen.getByText('value1: 1, value2: 1, value3: 1, value4: 1, value5: 1, value6: 1')
).toBeInTheDocument();
});
- expect(screen.getByTestId('renderCount')).toHaveTextContent('2');
+ expect(renderCount).toBe(2);
+ });
+
+ test('should batch state updates when using useBatchedOptionalPublishingSubjects', async () => {
+ let renderCount = 0;
+ function Component() {
+ const [value1, value2, value3, value4, value5, value6] =
+ useBatchedOptionalPublishingSubjects(
+ subject1,
+ subject2,
+ subject3,
+ subject4,
+ subject5,
+ subject6
+ );
+
+ renderCount++;
+ return (
+ <>
+ {
+ // using setTimeout to move next calls outside of callstack from onClick
+ setTimeout(incrementAll, 0);
+ }}
+ />
+ {`value1: ${value1}, value2: ${value2}, value3: ${value3}, value4: ${value4}, value5: ${value5}, value6: ${value6}`}
+ >
+ );
+ }
+ render( );
+ await waitFor(() => {
+ expect(
+ screen.getByText('value1: 0, value2: 0, value3: 0, value4: 0, value5: 0, value6: 0')
+ ).toBeInTheDocument();
+ });
+ userEvent.click(screen.getByRole('button'));
+ await waitFor(() => {
+ expect(
+ screen.getByText('value1: 1, value2: 1, value3: 1, value4: 1, value5: 1, value6: 1')
+ ).toBeInTheDocument();
+ });
+ expect(renderCount).toBe(2);
});
test('should render for each state update outside of click handler', async () => {
@@ -136,7 +178,6 @@ describe('useBatchedPublishingSubjects', () => {
}}
/>
{`value1: ${value1}, value2: ${value2}, value3: ${value3}, value4: ${value4}, value5: ${value5}, value6: ${value6}`}
- {renderCount}
>
);
}
@@ -152,33 +193,33 @@ describe('useBatchedPublishingSubjects', () => {
screen.getByText('value1: 1, value2: 1, value3: 1, value4: 1, value5: 1, value6: 1')
).toBeInTheDocument();
});
- expect(screen.getByTestId('renderCount')).toHaveTextContent('7');
+ expect(renderCount).toBe(7);
});
});
describe('Publishing subject is undefined on first render', () => {
- test('useBatchedPublishingSubjects state should update when publishing subject is provided', async () => {
+ test('useBatchedOptionalPublishingSubjects should update state when publishing subject is provided', async () => {
let renderCount = 0;
function Component() {
// When subjects is expected to change, subjects must be part of react state.
- const [subjectFoo, setSubjectFoo] = useState | undefined>(
+ const [subjectFoo, setSubjectFoo] = useState | undefined>(
undefined
);
- const [valueFoo] = useBatchedPublishingSubjects(subjectFoo);
+ const [valueFoo] = useBatchedOptionalPublishingSubjects(subjectFoo);
renderCount++;
return (
<>
{
- // using setTimeout to move next calls outside of callstack from onClick
- setTimeout(() => {
- setSubjectFoo(new BehaviorSubject('foo'));
- }, 0);
+ if (!subjectFoo) {
+ setSubjectFoo(new BehaviorSubject('foo1'));
+ } else {
+ subjectFoo.next('foo2');
+ }
}}
/>
{`valueFoo: ${valueFoo}`}
- {renderCount}
>
);
}
@@ -187,13 +228,14 @@ describe('useBatchedPublishingSubjects', () => {
expect(screen.getByText('valueFoo: undefined')).toBeInTheDocument();
});
userEvent.click(screen.getByRole('button'));
+ userEvent.click(screen.getByRole('button'));
await waitFor(() => {
- expect(screen.getByText('valueFoo: foo')).toBeInTheDocument();
+ expect(screen.getByText('valueFoo: foo2')).toBeInTheDocument();
});
- expect(screen.getByTestId('renderCount')).toHaveTextContent('3');
+ expect(renderCount).toBe(4);
});
- test('useStateFromPublishingSubject state should update when publishing subject is provided', async () => {
+ test('useStateFromPublishingSubject should update state when publishing subject is provided', async () => {
let renderCount = 0;
function Component() {
// When subject is expected to change, subject must be part of react state.
@@ -207,14 +249,10 @@ describe('useBatchedPublishingSubjects', () => {
<>
{
- // using setTimeout to move next calls outside of callstack from onClick
- setTimeout(() => {
- setSubjectFoo(new BehaviorSubject('foo'));
- }, 0);
+ setSubjectFoo(new BehaviorSubject('foo'));
}}
/>
{`valueFoo: ${valueFoo}`}
- {renderCount}
>
);
}
@@ -226,7 +264,7 @@ describe('useBatchedPublishingSubjects', () => {
await waitFor(() => {
expect(screen.getByText('valueFoo: foo')).toBeInTheDocument();
});
- expect(screen.getByTestId('renderCount')).toHaveTextContent('3');
+ expect(renderCount).toBe(3);
});
});
});
diff --git a/src/plugins/image_embeddable/public/components/image_embeddable.tsx b/src/plugins/image_embeddable/public/components/image_embeddable.tsx
index 542ac7dcbd965..f028ab5e16a62 100644
--- a/src/plugins/image_embeddable/public/components/image_embeddable.tsx
+++ b/src/plugins/image_embeddable/public/components/image_embeddable.tsx
@@ -10,6 +10,7 @@ import React, { useEffect, useState } from 'react';
import { PublishingSubject, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
+import { BehaviorSubject } from 'rxjs';
import { imageClickTrigger } from '../actions';
import { ImageEmbeddableApi } from '../image_embeddable/types';
import { FileImageMetadata, FilesClient, imageEmbeddableFileKind } from '../imports';
@@ -32,7 +33,7 @@ interface ImageEmbeddableProps {
export const ImageEmbeddable = ({ api, filesClient }: ImageEmbeddableProps) => {
const [imageConfig, dynamicActionsState] = useBatchedPublishingSubjects(
api.imageConfig$,
- api.dynamicActionsState$
+ api.dynamicActionsState$ ?? new BehaviorSubject(undefined)
);
const [hasTriggerActions, setHasTriggerActions] = useState(false);
diff --git a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_context_menu.tsx b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_context_menu.tsx
index 6c5aebf3154b7..9bafe438c4a72 100644
--- a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_context_menu.tsx
+++ b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_context_menu.tsx
@@ -21,7 +21,10 @@ import {
} from '@elastic/eui';
import { Action, buildContextMenuForActions } from '@kbn/ui-actions-plugin/public';
-import { getViewModeSubject, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
+import {
+ getViewModeSubject,
+ useBatchedOptionalPublishingSubjects,
+} from '@kbn/presentation-publishing';
import { uiActions } from '../../kibana_services';
import { contextMenuTrigger, CONTEXT_MENU_TRIGGER } from '../../panel_actions';
import { getContextMenuAriaLabel } from '../presentation_panel_strings';
@@ -43,7 +46,7 @@ export const PresentationPanelContextMenu = ({
const [isContextMenuOpen, setIsContextMenuOpen] = useState(undefined);
const [contextMenuPanels, setContextMenuPanels] = useState([]);
- const [title, parentViewMode] = useBatchedPublishingSubjects(
+ const [title, parentViewMode] = useBatchedOptionalPublishingSubjects(
api.panelTitle,
/**
diff --git a/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.tsx b/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.tsx
index f9be951c50997..da74df952b7e7 100644
--- a/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.tsx
+++ b/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.tsx
@@ -12,7 +12,7 @@ import {
apiPublishesPhaseEvents,
apiHasParentApi,
apiPublishesViewMode,
- useBatchedPublishingSubjects,
+ useBatchedOptionalPublishingSubjects,
} from '@kbn/presentation-publishing';
import classNames from 'classnames';
import React, { useEffect, useMemo, useState } from 'react';
@@ -57,7 +57,7 @@ export const PresentationPanelInternal = <
defaultPanelTitle,
rawViewMode,
parentHidePanelTitle,
- ] = useBatchedPublishingSubjects(
+ ] = useBatchedOptionalPublishingSubjects(
api?.dataLoading,
api?.blockingError,
api?.panelTitle,
From 48aed6e62364d10e9f708f3432df6e5157b19e8d Mon Sep 17 00:00:00 2001
From: Dzmitry Lemechko
Date: Wed, 10 Apr 2024 16:33:00 +0200
Subject: [PATCH 12/55] [FTR] add dataViews service (#180066)
## Summary
This PR makes few changes:
- extend
[kbn-test-subj-selector](https://github.com/elastic/kibana/compare/main...dmlemeshko:kibana:ftr/add-dataViews-service?expand=1#diff-43f2401dd3f9c11b6cbd75c8801a6ccbbe1d4db5a19e907f263c4932f810c73c)
with wildcard (*) support:
```
testSubjSelector('*dataView-switch-link') => [data-test-subj*="dataView-switch-link"]
```
It allows us to search for DOM elements with the common text part in
`data-test-subj` attribute, e.g. `lns-dataView-switch-link` &
`discover-dataView-switch-link`
- add new FTR service
[dataViews](test/functional/apps/discover/group4/_adhoc_data_views.ts)
to unify mostly identical code related to data views across multiple
page objects (lens, discover, unified_search, dashboard)
It is not a big win in terms of code cleanup, but should save some time
whenever we will need to make logic updates.
---
.../test_subj_selector.test.ts | 4 +
.../test_subj_selector.ts | 24 ++-
.../apps/dashboard/group4/dashboard_empty.ts | 19 +-
.../ccs_compatibility/_data_view_editor.ts | 27 +--
.../ccs_compatibility/_saved_queries.ts | 9 +-
.../ccs_compatibility/_search_errors.ts | 5 +-
.../apps/discover/group1/_greeting_screen.ts | 22 +--
.../apps/discover/group1/_no_data.ts | 30 +---
.../discover/group2/_data_grid_new_line.ts | 3 +-
.../apps/discover/group3/_default_columns.ts | 38 +---
.../apps/discover/group3/_lens_vis.ts | 24 +--
.../apps/discover/group3/_panels_toggle.ts | 14 +-
.../apps/discover/group3/_sidebar.ts | 25 +--
.../discover/group3/_time_field_column.ts | 8 +-
.../apps/discover/group4/_adhoc_data_views.ts | 27 ++-
.../apps/discover/group4/_data_view_edit.ts | 29 +--
.../discover/group4/_field_list_new_fields.ts | 19 +-
.../apps/discover/group4/_new_search.ts | 13 +-
.../discover/group4/_runtime_fields_editor.ts | 4 +-
.../apps/visualize/group1/_no_data.ts | 18 +-
test/functional/page_objects/discover_page.ts | 45 +----
.../page_objects/unified_search_page.ts | 63 -------
test/functional/services/data_views.ts | 165 ++++++++++++++++++
test/functional/services/index.ts | 2 +
.../apps/discover/visualize_field.ts | 16 +-
.../apps/lens/group1/ad_hoc_data_view.ts | 18 +-
.../apps/lens/group2/field_formatters.ts | 16 +-
.../apps/lens/group2/fields_list.ts | 33 ++--
.../apps/lens/group2/persistent_context.ts | 5 +-
.../apps/lens/group3/runtime_fields.ts | 5 +-
.../functional/apps/lens/group5/formula.ts | 3 +-
.../functional/apps/lens/group6/no_data.ts | 31 +---
.../index_data_visualizer_filters.ts | 4 +-
...index_data_visualizer_grid_in_dashboard.ts | 5 +-
.../index_data_visualizer_grid_in_discover.ts | 7 +-
.../data_source_selector.ts | 7 +-
.../header_menu.ts | 5 +-
.../test/functional/page_objects/lens_page.ts | 18 --
.../services/ml/dashboard_embeddables.ts | 14 +-
.../functional/services/transform/discover.ts | 6 +-
.../discover/search_source_alert.ts | 33 ++--
.../services/deployment_agnostic_services.ts | 1 +
.../discover/group2/_adhoc_data_views.ts | 26 ++-
.../common/discover/group3/_request_counts.ts | 3 +-
.../common/discover/group3/_sidebar.ts | 24 +--
.../common/discover/x_pack/visualize_field.ts | 14 +-
.../discover/search_source_alert.ts | 26 +--
.../data_source_selector.ts | 7 +-
.../header_menu.ts | 10 +-
49 files changed, 457 insertions(+), 517 deletions(-)
create mode 100644 test/functional/services/data_views.ts
diff --git a/packages/kbn-test-subj-selector/test_subj_selector.test.ts b/packages/kbn-test-subj-selector/test_subj_selector.test.ts
index d8ad1e6ddfd56..e54cb516ba89a 100644
--- a/packages/kbn-test-subj-selector/test_subj_selector.test.ts
+++ b/packages/kbn-test-subj-selector/test_subj_selector.test.ts
@@ -13,6 +13,10 @@ describe('testSubjSelector()', function () {
expect(subj('foo bar')).toEqual('[data-test-subj="foo bar"]');
expect(subj('foo > bar')).toEqual('[data-test-subj="foo"] [data-test-subj="bar"]');
expect(subj('foo > bar baz')).toEqual('[data-test-subj="foo"] [data-test-subj="bar baz"]');
+ expect(subj('*foo')).toEqual('[data-test-subj*="foo"]');
+ expect(subj('foo*')).toEqual('[data-test-subj="foo*"]');
+ expect(subj('foo*bar')).toEqual('[data-test-subj="foo*bar"]');
+ expect(subj('*foo >* bar')).toEqual('[data-test-subj*="foo"] [data-test-subj*="bar"]');
expect(subj('foo> ~bar')).toEqual('[data-test-subj="foo"] [data-test-subj~="bar"]');
expect(subj('~ foo')).toEqual('[data-test-subj~="foo"]');
expect(subj('~foo & ~ bar')).toEqual('[data-test-subj~="foo"][data-test-subj~="bar"]');
diff --git a/packages/kbn-test-subj-selector/test_subj_selector.ts b/packages/kbn-test-subj-selector/test_subj_selector.ts
index e6db49755a89b..ca6bf569db68e 100644
--- a/packages/kbn-test-subj-selector/test_subj_selector.ts
+++ b/packages/kbn-test-subj-selector/test_subj_selector.ts
@@ -8,6 +8,7 @@
function selectorToTerms(selector: string) {
return selector
+ .replace(/\s*\*\s*/g, '*') // css locator with '*' operator cannot contain spaces
.replace(/\s*~\s*/g, '~') // css locator with '~' operator cannot contain spaces
.replace(/\s*>\s*/g, '>') // remove all whitespace around joins >
.replace(/\s*&\s*/g, '&') // remove all whitespace around joins &
@@ -16,9 +17,13 @@ function selectorToTerms(selector: string) {
function termToCssSelector(term: string) {
if (term) {
- return term.startsWith('~')
- ? '[data-test-subj~="' + term.substring(1).replace(/\s/g, '') + '"]'
- : '[data-test-subj="' + term + '"]';
+ if (term.startsWith('~')) {
+ return '[data-test-subj~="' + term.substring(1).replace(/\s/g, '') + '"]';
+ } else if (term.startsWith('*')) {
+ return '[data-test-subj*="' + term.substring(1).replace(/\s/g, '') + '"]';
+ } else {
+ return '[data-test-subj="' + term + '"]';
+ }
} else {
return '';
}
@@ -31,17 +36,28 @@ function termToCssSelector(term: string) {
*
* - `data-test-subj` values can include spaces
*
- * - prefixing a value with `~` will allow matching a single word in a `data-test-subj` that uses several space delimited list words
+ * - prefixing a value with `*` will allow matching a `data-test-subj` attribute containing at least one occurrence of value within the string.
+ * - example: `*foo`
+ * - css equivalent: `[data-test-subj*="foo"]`
+ * - DOM match example: data-test-subj="bar-foo"
+ *
+ * - prefixing a value with `~` will allow matching a `data-test-subj` attribute represented as a whitespace-separated list of words, one of which is exactly value
* - example: `~foo`
* - css equivalent: `[data-test-subj~="foo"]`
+ * - DOM match example: data-test-subj="foo bar"
*
* - the `>` character is used between two values to indicate that the value on the right must match an element inside an element matched by the value on the left
* - example: `foo > bar`
* - css equivalent: `[data-test-subj=foo] [data-test-subj=bar]`
+ * - DOM match example:
+ * data-test-subj="foo"
+ *
data-test-subj="bar"
+ *
*
* - the `&` character is used between two values to indicate that the value on both sides must both match the element
* - example: `foo & bar`
* - css equivalent: `[data-test-subj=foo][data-test-subj=bar]`
+ * - DOM match example: data-test-subj="foo bar"
*/
export function subj(selector: string) {
return selectorToTerms(selector)
diff --git a/test/functional/apps/dashboard/group4/dashboard_empty.ts b/test/functional/apps/dashboard/group4/dashboard_empty.ts
index 03a9d965d589b..8cd11f4a7db86 100644
--- a/test/functional/apps/dashboard/group4/dashboard_empty.ts
+++ b/test/functional/apps/dashboard/group4/dashboard_empty.ts
@@ -11,21 +11,13 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const retry = getService('retry');
- const find = getService('find');
const filterBar = getService('filterBar');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'timePicker']);
- const createDataView = async (dataViewName: string) => {
- await testSubjects.setValue('createIndexPatternTitleInput', dataViewName, {
- clearWithKeyboard: true,
- typeCharByChar: true,
- });
- await testSubjects.click('saveIndexPatternButton');
- };
-
describe('dashboard empty state', () => {
const kbnDirectory = 'test/functional/fixtures/kbn_archiver/dashboard/current/kibana';
@@ -56,15 +48,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// create the new data view from the dashboards/create route in order to test that the dashboard is loaded properly as soon as the data view is created...
await PageObjects.common.navigateToApp('dashboards', { hash: '/create' });
- const button = await testSubjects.find('createDataViewButton');
- button.click();
- await retry.waitForWithTimeout('index pattern editor form to be visible', 15000, async () => {
- return await (await find.byClassName('indexPatternEditor__form')).isDisplayed();
- });
-
const dataViewToCreate = 'logstash';
- await createDataView(dataViewToCreate);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromPrompt({ name: dataViewToCreate });
await retry.waitForWithTimeout(
'filter manager to be able to create a filter with the new data view',
5000,
diff --git a/test/functional/apps/discover/ccs_compatibility/_data_view_editor.ts b/test/functional/apps/discover/ccs_compatibility/_data_view_editor.ts
index 1e9e13163cb9c..134ef7a414828 100644
--- a/test/functional/apps/discover/ccs_compatibility/_data_view_editor.ts
+++ b/test/functional/apps/discover/ccs_compatibility/_data_view_editor.ts
@@ -9,12 +9,11 @@
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const retry = getService('retry');
+ const dataViews = getService('dataViews');
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
const security = getService('security');
const config = getService('config');
- const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects([
'common',
'discover',
@@ -37,16 +36,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
? remoteArchiveDirectory
: localArchiveDirectory;
- const createDataView = async (dataViewName: string) => {
- await PageObjects.discover.clickIndexPatternActions();
- await PageObjects.unifiedSearch.clickCreateNewDataView();
- await testSubjects.setValue('createIndexPatternTitleInput', dataViewName, {
- clearWithKeyboard: true,
- typeCharByChar: true,
- });
- await testSubjects.click('saveIndexPatternButton');
- };
-
describe('discover integration with data view editor', function describeIndexTests() {
before(async function () {
const roles = config.get('esTestCluster.ccs')
@@ -69,18 +58,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('allows creating a new data view', async function () {
const dataViewToCreate = config.get('esTestCluster.ccs') ? 'ftr-remote:logstash' : 'logstash';
- await createDataView(dataViewToCreate);
- await PageObjects.header.waitUntilLoadingHasFinished();
- await retry.waitForWithTimeout(
- 'data view selector to include a newly created dataview',
- 5000,
- async () => {
- const dataViewTitle = await PageObjects.discover.getCurrentlySelectedDataView();
- // data view editor will add wildcard symbol by default
- // so we need to include it in our original title when comparing
- return dataViewTitle === `${dataViewToCreate}*`;
- }
- );
+ await dataViews.createFromSearchBar({ name: dataViewToCreate });
+ await dataViews.waitForSwitcherToBe(`${dataViewToCreate}*`);
});
});
}
diff --git a/test/functional/apps/discover/ccs_compatibility/_saved_queries.ts b/test/functional/apps/discover/ccs_compatibility/_saved_queries.ts
index 417d9ad1bbcf2..0152735dad6b1 100644
--- a/test/functional/apps/discover/ccs_compatibility/_saved_queries.ts
+++ b/test/functional/apps/discover/ccs_compatibility/_saved_queries.ts
@@ -21,6 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
const testSubjects = getService('testSubjects');
const config = getService('config');
+ const dataViews = getService('dataViews');
const localArchiveDirectories = {
nested: 'test/functional/fixtures/kbn_archiver/date_nested.json',
discover: 'test/functional/fixtures/kbn_archiver/discover.json',
@@ -54,7 +55,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
log.debug('set up a query with filters to save');
await PageObjects.common.setTime({ from, to });
await PageObjects.common.navigateToApp('discover');
- await PageObjects.discover.selectIndexPattern(logstashIndexPatternString);
+ await dataViews.switchToAndValidate(logstashIndexPatternString);
await retry.try(async function tryingForTime() {
const hitCount = await PageObjects.discover.getHitCount();
expect(hitCount).to.be('4,731');
@@ -120,14 +121,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false);
expect(await queryBar.getQueryString()).to.eql('');
- await PageObjects.discover.selectIndexPattern(dateNestedIndexPattern);
+ await dataViews.switchToAndValidate(dateNestedIndexPattern);
expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false);
expect(await queryBar.getQueryString()).to.eql('');
- await PageObjects.discover.selectIndexPattern(logstashIndexPatternString);
- const currentDataView = await PageObjects.discover.getCurrentlySelectedDataView();
- expect(currentDataView).to.be(logstashIndexPatternString);
+ await dataViews.switchToAndValidate(logstashIndexPatternString);
await retry.try(async function tryingForTime() {
const hitCount = await PageObjects.discover.getHitCount();
expect(hitCount).to.be('4,731');
diff --git a/test/functional/apps/discover/ccs_compatibility/_search_errors.ts b/test/functional/apps/discover/ccs_compatibility/_search_errors.ts
index da2d2b5567341..254bfe6423326 100644
--- a/test/functional/apps/discover/ccs_compatibility/_search_errors.ts
+++ b/test/functional/apps/discover/ccs_compatibility/_search_errors.ts
@@ -15,6 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
const testSubjects = getService('testSubjects');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
const isCcsTest = config.get('esTestCluster.ccs');
@@ -40,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('exception on single shard shows warning and results', async () => {
await PageObjects.common.navigateToApp('discover');
- await PageObjects.discover.selectIndexPattern(defaultIndex);
+ await dataViews.switchToAndValidate(defaultIndex);
await PageObjects.timePicker.setDefaultAbsoluteRange();
await retry.try(async () => {
const hitCount = await PageObjects.discover.getHitCount();
@@ -73,7 +74,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('exception on all shards shows error', async () => {
await PageObjects.common.navigateToApp('discover');
- await PageObjects.discover.selectIndexPattern(defaultIndex);
+ await dataViews.switchToAndValidate(defaultIndex);
await PageObjects.timePicker.setDefaultAbsoluteRange();
await retry.try(async () => {
const hitCount = await PageObjects.discover.getHitCount();
diff --git a/test/functional/apps/discover/group1/_greeting_screen.ts b/test/functional/apps/discover/group1/_greeting_screen.ts
index f218bc28fc0ce..ea9126cc1d5a5 100644
--- a/test/functional/apps/discover/group1/_greeting_screen.ts
+++ b/test/functional/apps/discover/group1/_greeting_screen.ts
@@ -11,11 +11,11 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const retry = getService('retry');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const esArchiver = getService('esArchiver');
const security = getService('security');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects(['common', 'header', 'discover', 'timePicker']);
const defaultSettings = {
defaultIndex: 'logstash-*',
@@ -47,23 +47,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.waitUntilLoadingHasFinished();
- await testSubjects.click('createDataViewButton');
+ await dataViews.createFromPrompt({ name: 'logs', hasTimeField: true });
+ expect(await dataViews.isAdHoc()).to.be(false);
- await testSubjects.setValue('createIndexPatternTitleInput', 'logs', {
- clearWithKeyboard: true,
- typeCharByChar: true,
- });
- await retry.waitFor('timestamp field loaded', async () => {
- const timestampField = await testSubjects.find('timestampField');
- return !(await timestampField.elementHasClass('euiComboBox-isDisabled'));
- });
- await testSubjects.click('saveIndexPatternButton');
- await PageObjects.header.waitUntilLoadingHasFinished();
- expect(await PageObjects.discover.isAdHocDataViewSelected()).to.be(false);
-
- await PageObjects.discover.createAdHocDataView('log', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
- expect(await PageObjects.discover.isAdHocDataViewSelected()).to.be(true);
+ await dataViews.createFromSearchBar({ name: 'log', adHoc: true, hasTimeField: true });
+ expect(await dataViews.isAdHoc()).to.be(true);
expect(await PageObjects.discover.getIndexPatterns()).to.eql(['log*\nTemporary', 'logs*']);
});
diff --git a/test/functional/apps/discover/group1/_no_data.ts b/test/functional/apps/discover/group1/_no_data.ts
index 1a80955068bd0..5c5b518166abc 100644
--- a/test/functional/apps/discover/group1/_no_data.ts
+++ b/test/functional/apps/discover/group1/_no_data.ts
@@ -10,22 +10,13 @@ import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
- const retry = getService('retry');
- const find = getService('find');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const dataGrid = getService('dataGrid');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
- const createDataView = async (dataViewName: string) => {
- await testSubjects.setValue('createIndexPatternTitleInput', dataViewName, {
- clearWithKeyboard: true,
- typeCharByChar: true,
- });
- await testSubjects.click('saveIndexPatternButton');
- };
-
describe('discover no data', () => {
const kbnDirectory = 'test/functional/fixtures/kbn_archiver/discover';
@@ -55,24 +46,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
await PageObjects.common.navigateToApp('discover');
- await testSubjects.click('createDataViewButton');
- await retry.waitForWithTimeout('data view editor form to be visible', 15000, async () => {
- return await (await find.byClassName('indexPatternEditor__form')).isDisplayed();
- });
-
const dataViewToCreate = 'logstash';
- await createDataView(dataViewToCreate);
- await PageObjects.header.waitUntilLoadingHasFinished();
- await retry.waitForWithTimeout(
- 'data view selector to include a newly created dataview',
- 5000,
- async () => {
- const dataViewTitle = await PageObjects.discover.getCurrentlySelectedDataView();
- // data view editor will add wildcard symbol by default
- // so we need to include it in our original title when comparing
- return dataViewTitle === `${dataViewToCreate}*`;
- }
- );
+ await dataViews.createFromPrompt({ name: dataViewToCreate });
+ await dataViews.waitForSwitcherToBe(`${dataViewToCreate}*`);
});
it('skips to Discover to try ES|QL', async () => {
diff --git a/test/functional/apps/discover/group2/_data_grid_new_line.ts b/test/functional/apps/discover/group2/_data_grid_new_line.ts
index 34855d14e10be..8049a86fc1164 100644
--- a/test/functional/apps/discover/group2/_data_grid_new_line.ts
+++ b/test/functional/apps/discover/group2/_data_grid_new_line.ts
@@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const dataGrid = getService('dataGrid');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects([
'settings',
'common',
@@ -62,7 +63,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.uiSettings.update(defaultSettings);
await PageObjects.common.navigateToApp('discover');
await PageObjects.discover.waitUntilSearchingHasFinished();
- await PageObjects.discover.createAdHocDataView(INDEX_NAME, false);
+ await dataViews.createFromSearchBar({ name: INDEX_NAME, adHoc: true, hasTimeField: false });
await PageObjects.discover.waitUntilSearchingHasFinished();
});
diff --git a/test/functional/apps/discover/group3/_default_columns.ts b/test/functional/apps/discover/group3/_default_columns.ts
index 14c95c349b72a..5935a9366534c 100644
--- a/test/functional/apps/discover/group3/_default_columns.ts
+++ b/test/functional/apps/discover/group3/_default_columns.ts
@@ -11,6 +11,7 @@ import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dataGrid = getService('dataGrid');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects([
'common',
'discover',
@@ -78,31 +79,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'DestCountry',
]);
- await PageObjects.unifiedSearch.switchDataView(
- 'discover-dataView-switch-link',
- 'Kibana Sample Data Logs (TSDB)',
- false
- );
+ await dataViews.switchToAndValidate('Kibana Sample Data Logs (TSDB)');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage();
await PageObjects.discover.waitUntilSearchingHasFinished();
expect(await dataGrid.getHeaderFields()).to.eql(['timestamp', 'message', 'extension']);
- await PageObjects.unifiedSearch.switchDataView(
- 'discover-dataView-switch-link',
- 'kibana_sample_data_flights',
- false
- );
+ await dataViews.switchToAndValidate('kibana_sample_data_flights');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage();
await PageObjects.discover.waitUntilSearchingHasFinished();
expect(await dataGrid.getHeaderFields()).to.eql(['timestamp', 'DestCountry']);
- await PageObjects.unifiedSearch.switchDataView(
- 'discover-dataView-switch-link',
- 'logstash-*',
- false
- );
+ await dataViews.switchToAndValidate('logstash-*');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage();
await PageObjects.discover.waitUntilSearchingHasFinished();
@@ -116,12 +105,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
expect(await dataGrid.getHeaderFields()).to.eql(['@timestamp', 'extension', 'bytes']);
-
- await PageObjects.unifiedSearch.switchDataView(
- 'discover-dataView-switch-link',
- 'Kibana Sample Data Logs (TSDB)',
- false
- );
+ await dataViews.switchToAndValidate('Kibana Sample Data Logs (TSDB)');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage();
await PageObjects.discover.waitUntilSearchingHasFinished();
@@ -132,11 +116,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'message',
]);
- await PageObjects.unifiedSearch.switchDataView(
- 'discover-dataView-switch-link',
- 'logstash-*',
- false
- );
+ await dataViews.switchToAndValidate('logstash-*');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage();
await PageObjects.discover.waitUntilSearchingHasFinished();
@@ -157,11 +137,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'DestCountry',
]);
- await PageObjects.unifiedSearch.switchDataView(
- 'discover-dataView-switch-link',
- 'kibana_sample_data_flights',
- false
- );
+ await dataViews.switchToAndValidate('kibana_sample_data_flights');
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage();
await PageObjects.discover.waitUntilSearchingHasFinished();
diff --git a/test/functional/apps/discover/group3/_lens_vis.ts b/test/functional/apps/discover/group3/_lens_vis.ts
index d83cbe992e929..3a536fe655815 100644
--- a/test/functional/apps/discover/group3/_lens_vis.ts
+++ b/test/functional/apps/discover/group3/_lens_vis.ts
@@ -18,16 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const find = getService('find');
const browser = getService('browser');
const toasts = getService('toasts');
- const PageObjects = getPageObjects([
- 'settings',
- 'common',
- 'discover',
- 'header',
- 'timePicker',
- 'dashboard',
- 'unifiedFieldList',
- 'unifiedSearch',
- ]);
+ const dataViews = getService('dataViews');
+ const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
const security = getService('security');
const defaultSettings = {
defaultIndex: 'logstash-*',
@@ -172,13 +164,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should show no histogram for non-time-based data views and recover for time-based data views', async () => {
- await PageObjects.discover.createAdHocDataView('logs*', false);
-
+ await dataViews.createFromSearchBar({
+ name: 'logs',
+ adHoc: true,
+ hasTimeField: true,
+ changeTimestampField: `--- I don't want to use the time filter ---`,
+ });
await checkNoVis(defaultTotalCount);
- await PageObjects.discover.clickIndexPatternActions();
- await PageObjects.unifiedSearch.editDataView('logs*', '@timestamp');
-
+ await dataViews.editFromSearchBar({ newName: 'logs', newTimeField: '@timestamp' });
await checkHistogramVis(defaultTimespan, defaultTotalCount);
});
diff --git a/test/functional/apps/discover/group3/_panels_toggle.ts b/test/functional/apps/discover/group3/_panels_toggle.ts
index d471969d3528f..f2cc179b58fa3 100644
--- a/test/functional/apps/discover/group3/_panels_toggle.ts
+++ b/test/functional/apps/discover/group3/_panels_toggle.ts
@@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const monacoEditor = getService('monacoEditor');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects([
'settings',
'common',
@@ -205,7 +206,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.uiSettings.update(defaultSettings);
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.waitUntilLoadingHasFinished();
- await PageObjects.discover.createAdHocDataView('log*', false);
+ await dataViews.createFromSearchBar({
+ name: 'log*',
+ adHoc: true,
+ hasTimeField: false,
+ });
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
});
@@ -249,8 +254,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.uiSettings.update(defaultSettings);
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.waitUntilLoadingHasFinished();
- await PageObjects.discover.createAdHocDataView('log*', false);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromSearchBar({
+ name: 'log*',
+ adHoc: true,
+ hasTimeField: false,
+ });
await PageObjects.discover.selectTextBaseLang();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
});
diff --git a/test/functional/apps/discover/group3/_sidebar.ts b/test/functional/apps/discover/group3/_sidebar.ts
index d536e09920590..2605f2bf93559 100644
--- a/test/functional/apps/discover/group3/_sidebar.ts
+++ b/test/functional/apps/discover/group3/_sidebar.ts
@@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const monacoEditor = getService('monacoEditor');
const filterBar = getService('filterBar');
const fieldEditor = getService('fieldEditor');
+ const dataViews = getService('dataViews');
const retry = getService('retry');
const dataGrid = getService('dataGrid');
const INITIAL_FIELD_LIST_SUMMARY = '48 available fields. 5 empty fields. 3 meta fields.';
@@ -490,7 +491,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
INITIAL_FIELD_LIST_SUMMARY
);
- await PageObjects.discover.selectIndexPattern('with-timefield');
+ await dataViews.switchToAndValidate('with-timefield');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -507,7 +508,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
)}NoFieldsCallout-noFieldsExist`
);
- await PageObjects.discover.selectIndexPattern('logstash-*');
+ await dataViews.switchToAndValidate('logstash-*');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -535,7 +536,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
INITIAL_FIELD_LIST_SUMMARY
);
- await PageObjects.discover.selectIndexPattern('without-timefield');
+ await dataViews.switchToAndValidate('without-timefield');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -544,7 +545,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'6 available fields. 3 meta fields.'
);
- await PageObjects.discover.selectIndexPattern('with-timefield');
+ await dataViews.switchToAndValidate('with-timefield');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -558,7 +559,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
)}NoFieldsCallout-noFieldsMatch`
);
- await PageObjects.discover.selectIndexPattern('logstash-*');
+ await dataViews.switchToAndValidate('logstash-*');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -614,7 +615,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
INITIAL_FIELD_LIST_SUMMARY
);
- await PageObjects.discover.selectIndexPattern('indices-stats*');
+ await dataViews.switchToAndValidate('indices-stats*');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -623,7 +624,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'6873 available fields. 3 meta fields.'
);
- await PageObjects.discover.selectIndexPattern('logstash-*');
+ await dataViews.switchToAndValidate('logstash-*');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -639,9 +640,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should work with ad-hoc data views and runtime fields', async () => {
- await PageObjects.discover.createAdHocDataView('logstash', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
-
+ await dataViews.createFromSearchBar({
+ name: 'logstash',
+ adHoc: true,
+ hasTimeField: true,
+ });
expect(await PageObjects.unifiedFieldList.getSidebarAriaDescription()).to.be(
INITIAL_FIELD_LIST_SUMMARY
);
@@ -749,7 +752,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
INITIAL_FIELD_LIST_SUMMARY
);
- await PageObjects.discover.selectIndexPattern('with-timefield');
+ await dataViews.switchToAndValidate('with-timefield');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
diff --git a/test/functional/apps/discover/group3/_time_field_column.ts b/test/functional/apps/discover/group3/_time_field_column.ts
index 8ed5188151bad..3a916f99f8fbe 100644
--- a/test/functional/apps/discover/group3/_time_field_column.ts
+++ b/test/functional/apps/discover/group3/_time_field_column.ts
@@ -30,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dashboardAddPanel = getService('dashboardAddPanel');
const dashboardPanelActions = getService('dashboardPanelActions');
const monacoEditor = getService('monacoEditor');
+ const dataViews = getService('dataViews');
const testSubjects = getService('testSubjects');
const security = getService('security');
const docTable = getService('docTable');
@@ -257,9 +258,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('without a time field', () => {
beforeEach(async () => {
- await PageObjects.discover.createAdHocDataView('logs*', false);
+ await dataViews.createFromSearchBar({
+ name: 'logs*',
+ adHoc: true,
+ hasTimeField: false,
+ });
await PageObjects.discover.waitUntilSearchingHasFinished();
- await PageObjects.header.waitUntilLoadingHasFinished();
});
it('should render initial columns correctly', async () => {
diff --git a/test/functional/apps/discover/group4/_adhoc_data_views.ts b/test/functional/apps/discover/group4/_adhoc_data_views.ts
index f84b69d2184a1..4a25441ad531f 100644
--- a/test/functional/apps/discover/group4/_adhoc_data_views.ts
+++ b/test/functional/apps/discover/group4/_adhoc_data_views.ts
@@ -34,6 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
]);
const find = getService('find');
const security = getService('security');
+ const dataViews = getService('dataViews');
const addSearchToDashboard = async (name: string) => {
await dashboardAddPanel.addSavedSearch(name);
@@ -57,8 +58,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should navigate back correctly from to surrounding and single views', async () => {
- await PageObjects.discover.createAdHocDataView('logstash', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromSearchBar({
+ name: 'logstash',
+ adHoc: true,
+ hasTimeField: true,
+ });
const first = await PageObjects.discover.getCurrentDataViewId();
await PageObjects.discover.addRuntimeField(
@@ -79,7 +83,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('~breadcrumb & ~first');
await PageObjects.header.waitUntilLoadingHasFinished();
- expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.be('logstash*');
+ expect(await dataViews.getSelectedName()).to.be('logstash*');
// navigate to single doc view
await dataGrid.clickRowToggle({ rowIndex: 0 });
@@ -90,7 +94,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('~breadcrumb & ~first');
await PageObjects.header.waitUntilLoadingHasFinished();
- expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.be('logstash*');
+ expect(await dataViews.getSelectedName()).to.be('logstash*');
});
it('should support query and filtering', async () => {
@@ -136,8 +140,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('search results should be different after data view update', async () => {
- await PageObjects.discover.createAdHocDataView('logst', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromSearchBar({
+ name: 'logst',
+ adHoc: true,
+ hasTimeField: true,
+ });
const prevDataViewId = await PageObjects.discover.getCurrentDataViewId();
// trigger data view id update
@@ -231,9 +238,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should notify about invalid filter reffs', async () => {
- await PageObjects.discover.createAdHocDataView('logstas', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
-
+ await dataViews.createFromSearchBar({
+ name: 'logstas',
+ adHoc: true,
+ hasTimeField: true,
+ });
await filterBar.addFilter({
field: 'nestedField.child',
operation: 'is',
diff --git a/test/functional/apps/discover/group4/_data_view_edit.ts b/test/functional/apps/discover/group4/_data_view_edit.ts
index c4cc4f796e884..381165d1d64f8 100644
--- a/test/functional/apps/discover/group4/_data_view_edit.ts
+++ b/test/functional/apps/discover/group4/_data_view_edit.ts
@@ -15,6 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const security = getService('security');
const es = getService('es');
const retry = getService('retry');
+ const dataViews = getService('dataViews');
+
const PageObjects = getPageObjects([
'common',
'unifiedSearch',
@@ -72,11 +74,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
},
});
- await PageObjects.discover.createAdHocDataView(initialPattern, true);
-
- await retry.waitFor('current data view to get updated', async () => {
- return (await PageObjects.discover.getCurrentlySelectedDataView()) === `${initialPattern}*`;
+ await dataViews.createFromSearchBar({
+ name: initialPattern,
+ adHoc: true,
+ hasTimeField: true,
});
+ await dataViews.waitForSwitcherToBe(`${initialPattern}*`);
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
expect(await PageObjects.discover.getHitCountInt()).to.be(2);
@@ -85,8 +88,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('create saved data view', async function () {
const updatedPattern = 'my-index-000001';
- await PageObjects.discover.clickIndexPatternActions();
- await PageObjects.unifiedSearch.createNewDataView(updatedPattern, false, true);
+ await dataViews.createFromSearchBar({
+ name: updatedPattern,
+ adHoc: false,
+ hasTimeField: true,
+ });
await retry.try(async () => {
expect(await PageObjects.discover.getHitCountInt()).to.be(1);
@@ -118,8 +124,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
},
});
}
- await PageObjects.discover.clickIndexPatternActions();
- await PageObjects.unifiedSearch.editDataView(updatedPattern, 'timestamp');
+ await dataViews.editFromSearchBar({ newName: updatedPattern, newTimeField: 'timestamp' });
await retry.try(async () => {
expect(await PageObjects.discover.getHitCountInt()).to.be(3);
});
@@ -130,11 +135,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('update data view with no time field', async function () {
- await PageObjects.discover.clickIndexPatternActions();
- await PageObjects.unifiedSearch.editDataView(
- undefined,
- "--- I don't want to use the time filter ---"
- );
+ await dataViews.editFromSearchBar({
+ newTimeField: "--- I don't want to use the time filter ---",
+ });
await retry.try(async () => {
expect(await PageObjects.discover.getHitCountInt()).to.be(4);
});
diff --git a/test/functional/apps/discover/group4/_field_list_new_fields.ts b/test/functional/apps/discover/group4/_field_list_new_fields.ts
index 4646a57600b4a..b523aa433db6e 100644
--- a/test/functional/apps/discover/group4/_field_list_new_fields.ts
+++ b/test/functional/apps/discover/group4/_field_list_new_fields.ts
@@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const es = getService('es');
const retry = getService('retry');
const queryBar = getService('queryBar');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'unifiedFieldList']);
describe('Field list new fields in background handling', function () {
@@ -52,11 +53,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
},
});
- await PageObjects.discover.createAdHocDataView(initialPattern, true);
-
- await retry.waitFor('current data view to get updated', async () => {
- return (await PageObjects.discover.getCurrentlySelectedDataView()) === `${initialPattern}*`;
+ await dataViews.createFromSearchBar({
+ name: initialPattern,
+ adHoc: true,
+ hasTimeField: true,
});
+ await dataViews.waitForSwitcherToBe(`${initialPattern}*`);
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
expect(await PageObjects.discover.getHitCountInt()).to.be(1);
@@ -98,11 +100,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
},
});
- await PageObjects.discover.createAdHocDataView(initialPattern, true);
-
- await retry.waitFor('current data view to get updated', async () => {
- return (await PageObjects.discover.getCurrentlySelectedDataView()) === `${initialPattern}*`;
+ await dataViews.createFromSearchBar({
+ name: initialPattern,
+ adHoc: true,
+ hasTimeField: true,
});
+ await dataViews.waitForSwitcherToBe(`${initialPattern}*`);
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
expect(await PageObjects.discover.getHitCountInt()).to.be(1);
diff --git a/test/functional/apps/discover/group4/_new_search.ts b/test/functional/apps/discover/group4/_new_search.ts
index 2e78a2e3c9901..0b670842e027b 100644
--- a/test/functional/apps/discover/group4/_new_search.ts
+++ b/test/functional/apps/discover/group4/_new_search.ts
@@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const monacoEditor = getService('monacoEditor');
const testSubjects = getService('testSubjects');
const security = getService('security');
+ const dataViews = getService('dataViews');
describe('discover new search action', function () {
before(async function () {
@@ -61,7 +62,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should work correctly for a saved search in data view mode', async function () {
- await PageObjects.discover.createAdHocDataView('logs*', true);
+ await dataViews.createFromSearchBar({
+ name: 'logs*',
+ adHoc: true,
+ hasTimeField: true,
+ });
await filterBar.addFilter({ field: 'extension', operation: 'is', value: 'css' });
await PageObjects.header.waitUntilLoadingHasFinished();
await queryBar.setQuery('bytes > 100');
@@ -81,10 +86,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.discover.getHitCount()).to.be('14,004');
expect(await filterBar.hasFilter('extension', 'css')).to.be(false);
expect(await queryBar.getQueryString()).to.be('');
- expect(
- await PageObjects.unifiedSearch.getSelectedDataView('discover-dataView-switch-link')
- ).to.be('logs**');
- expect(await PageObjects.discover.isAdHocDataViewSelected()).to.be(true);
+ expect(await dataViews.getSelectedName()).to.be('logs**');
+ expect(await dataViews.isAdHoc()).to.be(true);
});
it('should work correctly for ESQL mode', async () => {
diff --git a/test/functional/apps/discover/group4/_runtime_fields_editor.ts b/test/functional/apps/discover/group4/_runtime_fields_editor.ts
index 1e79a2b829fa4..de43dc8e74e92 100644
--- a/test/functional/apps/discover/group4/_runtime_fields_editor.ts
+++ b/test/functional/apps/discover/group4/_runtime_fields_editor.ts
@@ -17,6 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const fieldEditor = getService('fieldEditor');
const security = getService('security');
const dataGrid = getService('dataGrid');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects([
'common',
'discover',
@@ -30,8 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
};
const createRuntimeField = async (fieldName: string) => {
- await PageObjects.discover.clickIndexPatternActions();
- await PageObjects.discover.clickAddNewField();
+ await dataViews.clickAddFieldFromSearchBar();
await fieldEditor.setName(fieldName);
await fieldEditor.enableValue();
await fieldEditor.typeScript("emit('abc')");
diff --git a/test/functional/apps/visualize/group1/_no_data.ts b/test/functional/apps/visualize/group1/_no_data.ts
index 33b69de9a91c6..64d1a3c138ac9 100644
--- a/test/functional/apps/visualize/group1/_no_data.ts
+++ b/test/functional/apps/visualize/group1/_no_data.ts
@@ -13,17 +13,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['visualize', 'header', 'common']);
const esArchiver = getService('esArchiver');
- const find = getService('find');
+ const dataViews = getService('dataViews');
const kibanaServer = getService('kibanaServer');
- const createDataView = async (dataViewName: string) => {
- await testSubjects.setValue('createIndexPatternTitleInput', dataViewName, {
- clearWithKeyboard: true,
- typeCharByChar: true,
- });
- await testSubjects.click('saveIndexPatternButton');
- };
-
describe('no data in visualize', function () {
it('should show the integrations component if there is no data', async () => {
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
@@ -43,15 +35,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
await PageObjects.common.navigateToApp('visualize');
await PageObjects.header.waitUntilLoadingHasFinished();
- const button = await testSubjects.find('createDataViewButton');
- button.click();
- await retry.waitForWithTimeout('index pattern editor form to be visible', 15000, async () => {
- return await (await find.byClassName('indexPatternEditor__form')).isDisplayed();
- });
const dataViewToCreate = 'logstash';
- await createDataView(dataViewToCreate);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromPrompt({ name: dataViewToCreate });
await retry.waitForWithTimeout(
'data view selector to include a newly created dataview',
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index 47165f90952ee..b7de38b825346 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -16,7 +16,7 @@ export class DiscoverPageObject extends FtrService {
private readonly find = this.ctx.getService('find');
private readonly flyout = this.ctx.getService('flyout');
private readonly header = this.ctx.getPageObject('header');
- private readonly unifiedSearch = this.ctx.getPageObject('unifiedSearch');
+ private readonly dataViews = this.ctx.getService('dataViews');
private readonly unifiedFieldList = this.ctx.getPageObject('unifiedFieldList');
private readonly browser = this.ctx.getService('browser');
private readonly globalNav = this.ctx.getService('globalNav');
@@ -482,27 +482,6 @@ export class DiscoverPageObject extends FtrService {
});
}
- public async clickAddNewField() {
- await this.retry.try(async () => {
- await this.testSubjects.click('indexPattern-add-field');
- await this.find.byClassName('indexPatternFieldEditor__form');
- });
- }
-
- async createAdHocDataView(name: string, hasTimeField = false) {
- await this.testSubjects.click('discover-dataView-switch-link');
- await this.unifiedSearch.createNewDataView(name, true, hasTimeField);
- await this.retry.waitFor('flyout to get closed', async () => {
- return !(await this.testSubjects.exists('indexPatternEditor__form'));
- });
- }
-
- async clickAddField() {
- await this.testSubjects.click('discover-dataView-switch-link');
- await this.testSubjects.existOrFail('indexPattern-add-field');
- await this.testSubjects.click('indexPattern-add-field');
- }
-
public async hasNoResults() {
return await this.testSubjects.exists('discoverNoResults');
}
@@ -552,23 +531,11 @@ export class DiscoverPageObject extends FtrService {
return await this.dataGrid.clickDocSortAsc(field, text);
}
- public async isAdHocDataViewSelected() {
- const dataView = await this.getCurrentlySelectedDataView();
- await this.testSubjects.click('discover-dataView-switch-link');
- const hasBadge = await this.testSubjects.exists(`dataViewItemTempBadge-${dataView}`);
- await this.testSubjects.click('discover-dataView-switch-link');
- return hasBadge;
- }
-
public async selectIndexPattern(
indexPattern: string,
waitUntilLoadingHasFinished: boolean = true
) {
- await this.testSubjects.click('discover-dataView-switch-link');
- await this.find.setValue('[data-test-subj="indexPattern-switcher"] input', indexPattern);
- await this.find.clickByCssSelector(
- `[data-test-subj="indexPattern-switcher"] [title="${indexPattern}"]`
- );
+ await this.dataViews.switchTo(indexPattern);
if (waitUntilLoadingHasFinished) {
await this.header.waitUntilLoadingHasFinished();
}
@@ -711,12 +678,6 @@ export class DiscoverPageObject extends FtrService {
});
}
- public async getCurrentlySelectedDataView() {
- await this.testSubjects.existOrFail('discover-sidebar');
- const button = await this.testSubjects.find('discover-dataView-switch-link');
- return button.getAttribute('title');
- }
-
/**
* Validates if data view references in the URL are equal.
*/
@@ -755,7 +716,7 @@ export class DiscoverPageObject extends FtrService {
}
public async addRuntimeField(name: string, script: string, type?: string) {
- await this.clickAddField();
+ await this.dataViews.clickAddFieldFromSearchBar();
await this.fieldEditor.setName(name);
if (type) {
await this.fieldEditor.setFieldType(type);
diff --git a/test/functional/page_objects/unified_search_page.ts b/test/functional/page_objects/unified_search_page.ts
index 3ab2a2f1b54d6..eee70098d2ab1 100644
--- a/test/functional/page_objects/unified_search_page.ts
+++ b/test/functional/page_objects/unified_search_page.ts
@@ -12,7 +12,6 @@ export class UnifiedSearchPageObject extends FtrService {
private readonly retry = this.ctx.getService('retry');
private readonly testSubjects = this.ctx.getService('testSubjects');
private readonly find = this.ctx.getService('find');
- private readonly comboBox = this.ctx.getService('comboBox');
public async switchDataView(
switchButtonSelector: string,
@@ -46,68 +45,6 @@ export class UnifiedSearchPageObject extends FtrService {
return visibleText;
}
- private async modifyDataView(buttonLocator: string) {
- await this.retry.waitForWithTimeout('data create new to be visible', 15000, async () => {
- return await this.testSubjects.isDisplayed(buttonLocator);
- });
- await this.testSubjects.click(buttonLocator);
- await this.retry.waitForWithTimeout(
- 'index pattern editor form to be visible',
- 15000,
- async () => {
- return await (await this.find.byClassName('indexPatternEditor__form')).isDisplayed();
- }
- );
- await (await this.find.byClassName('indexPatternEditor__form')).click();
- }
-
- public async clickCreateNewDataView() {
- await this.modifyDataView('dataview-create-new');
- }
-
- public async clickEditDataView() {
- await this.modifyDataView('indexPattern-manage-field');
- }
-
- public async createNewDataView(dataViewPattern: string, adHoc = false, hasTimeField = false) {
- await this.clickCreateNewDataView();
- await this.testSubjects.setValue('createIndexPatternTitleInput', dataViewPattern, {
- clearWithKeyboard: true,
- typeCharByChar: true,
- });
- await this.retry.waitFor('timestamp field loaded', async () => {
- const timestampField = await this.testSubjects.find('timestampField');
- return hasTimeField
- ? !(await timestampField.elementHasClass('euiComboBox-isDisabled'))
- : true;
- });
- await this.testSubjects.click(adHoc ? 'exploreIndexPatternButton' : 'saveIndexPatternButton');
- }
-
- public async editDataView(newPattern?: string, newTimeField?: string) {
- await this.clickEditDataView();
- if (newPattern) {
- await this.testSubjects.setValue('createIndexPatternTitleInput', newPattern, {
- clearWithKeyboard: true,
- typeCharByChar: true,
- });
- }
- if (newTimeField) {
- await this.comboBox.set('timestampField', newTimeField);
- }
- await this.testSubjects.click('saveIndexPatternButton');
- if (await this.testSubjects.exists('confirmModalConfirmButton')) {
- await this.testSubjects.click('confirmModalConfirmButton');
- }
- }
-
- public async isAdHocDataView() {
- const dataViewSwitcher = await this.testSubjects.find('discover-dataView-switch-link');
- const dataViewName = await dataViewSwitcher.getVisibleText();
- await dataViewSwitcher.click();
- return await this.testSubjects.exists(`dataViewItemTempBadge-${dataViewName}`);
- }
-
public async selectTextBasedLanguage(language: string) {
await this.find.clickByCssSelector(
`[data-test-subj="text-based-languages-switcher"] [title="${language}"]`
diff --git a/test/functional/services/data_views.ts b/test/functional/services/data_views.ts
new file mode 100644
index 0000000000000..d63f47a89cdb0
--- /dev/null
+++ b/test/functional/services/data_views.ts
@@ -0,0 +1,165 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { FtrService } from '../ftr_provider_context';
+
+interface DataViewOptions {
+ name: string;
+ adHoc?: boolean;
+ hasTimeField?: boolean;
+ changeTimestampField?: string;
+}
+
+export class DataViewsService extends FtrService {
+ private readonly retry = this.ctx.getService('retry');
+ private readonly testSubjects = this.ctx.getService('testSubjects');
+ private readonly find = this.ctx.getService('find');
+ private readonly comboBox = this.ctx.getService('comboBox');
+ private readonly header = this.ctx.getPageObjects(['header']).header;
+
+ private async create({
+ name, // Data View title, * will be added automatically
+ adHoc = false, // pass 'true' to have temporary Data View
+ hasTimeField = false, // pass 'true' if Data View has timestamp field
+ changeTimestampField, // optionally override default timestamp field
+ }: DataViewOptions) {
+ await this.testSubjects.existOrFail('indexPatternEditorFlyout');
+ await this.testSubjects.setValue('createIndexPatternTitleInput', name, {
+ clearWithKeyboard: true,
+ typeCharByChar: true,
+ });
+ if (hasTimeField) {
+ await this.retry.waitFor('timestamp field loaded', async () => {
+ const timestampField = await this.testSubjects.find('timestampField');
+ return !(await timestampField.elementHasClass('euiComboBox-isDisabled'));
+ });
+
+ if (changeTimestampField) {
+ await this.comboBox.set('timestampField', changeTimestampField);
+ }
+ }
+ await this.testSubjects.click(adHoc ? 'exploreIndexPatternButton' : 'saveIndexPatternButton');
+ await this.header.waitUntilLoadingHasFinished();
+ }
+
+ /**
+ * Create a new Data View from top search bar
+ */
+ async createFromSearchBar({
+ name,
+ adHoc = false,
+ hasTimeField = false,
+ changeTimestampField,
+ }: DataViewOptions) {
+ await this.testSubjects.click('*dataView-switch-link');
+ await this.testSubjects.click('dataview-create-new');
+ await this.create({ name, adHoc, hasTimeField, changeTimestampField });
+ }
+
+ /**
+ * Create the first Data View from Prompt, e.g. on Dashboard
+ */
+ async createFromPrompt({
+ name,
+ adHoc = false,
+ hasTimeField = false,
+ changeTimestampField,
+ }: DataViewOptions) {
+ await this.testSubjects.click('createDataViewButton');
+ await this.create({ name, adHoc, hasTimeField, changeTimestampField });
+ }
+
+ /**
+ * Returns name for the currently selected Data View
+ */
+ async getSelectedName() {
+ return this.testSubjects.getVisibleText('*dataView-switch-link');
+ }
+
+ /**
+ * Checks if currently selected Data View has temporary badge
+ */
+ async isAdHoc() {
+ const dataView = await this.testSubjects.getAttribute('*dataView-switch-link', 'title');
+ await this.testSubjects.click('*dataView-switch-link');
+ const hasBadge = await this.testSubjects.exists(`dataViewItemTempBadge-${dataView}`);
+ await this.testSubjects.click('*dataView-switch-link');
+ return hasBadge;
+ }
+
+ /**
+ * Opens Create field flayout for the selected Data View
+ */
+ async clickAddFieldFromSearchBar() {
+ await this.testSubjects.click('*dataView-switch-link');
+ await this.testSubjects.click('indexPattern-add-field');
+ await this.testSubjects.existOrFail('fieldEditor');
+ }
+
+ /**
+ * Switch Data View from top search bar
+ */
+ public async switchTo(name: string) {
+ const selectedDataView = await this.getSelectedName();
+ if (name === selectedDataView) {
+ return;
+ }
+ await this.testSubjects.click('*dataView-switch-link');
+ await this.testSubjects.existOrFail('indexPattern-switcher');
+ await this.testSubjects.setValue('indexPattern-switcher--input', name);
+ await this.find.clickByCssSelector(
+ `[data-test-subj="indexPattern-switcher"] [title="${name}"]`
+ );
+ }
+
+ /**
+ * Waits for selected Data View to equal name argument
+ */
+ public async waitForSwitcherToBe(name: string) {
+ await this.retry.waitFor(
+ 'Data View switcher to be updated',
+ async () => (await this.getSelectedName()) === name
+ );
+ }
+
+ /**
+ * Switch Data View from top search bar and validate selection is applied
+ */
+ public async switchToAndValidate(name: string) {
+ await this.switchTo(name);
+ await this.waitForSwitcherToBe(name);
+ }
+
+ /**
+ * Edit currently selected Data View
+ */
+ public async editFromSearchBar({
+ newName,
+ newTimeField,
+ }: {
+ newName?: string;
+ newTimeField?: string;
+ }) {
+ await this.testSubjects.click('*dataView-switch-link');
+ await this.testSubjects.click('indexPattern-manage-field');
+ await this.testSubjects.existOrFail('indexPatternEditorFlyout');
+ if (newName) {
+ await this.testSubjects.setValue('createIndexPatternTitleInput', newName, {
+ clearWithKeyboard: true,
+ typeCharByChar: true,
+ });
+ }
+ if (newTimeField) {
+ await this.comboBox.set('timestampField', newTimeField);
+ }
+ await this.testSubjects.click('saveIndexPatternButton');
+ if (await this.testSubjects.exists('confirmModalConfirmButton')) {
+ await this.testSubjects.click('confirmModalConfirmButton');
+ }
+ }
+}
diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts
index 01d5983c55493..83672889eff75 100644
--- a/test/functional/services/index.ts
+++ b/test/functional/services/index.ts
@@ -54,6 +54,7 @@ import { UsageCollectionService } from './usage_collection';
import { SavedObjectsFinderService } from './saved_objects_finder';
import { DashboardSettingsProvider } from './dashboard/dashboard_settings';
import { ESQLService } from './esql';
+import { DataViewsService } from './data_views';
export const services = {
...commonServiceProviders,
@@ -76,6 +77,7 @@ export const services = {
dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider,
dashboardDrilldownsManage: DashboardDrilldownsManageProvider,
dashboardSettings: DashboardSettingsProvider,
+ dataViews: DataViewsService,
flyout: FlyoutService,
comboBox: ComboBoxService,
selectable: SelectableService,
diff --git a/x-pack/test/functional/apps/discover/visualize_field.ts b/x-pack/test/functional/apps/discover/visualize_field.ts
index 8df6c9541f0dc..d8444eefc8798 100644
--- a/x-pack/test/functional/apps/discover/visualize_field.ts
+++ b/x-pack/test/functional/apps/discover/visualize_field.ts
@@ -17,15 +17,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const kibanaServer = getService('kibanaServer');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects([
'common',
- 'error',
'discover',
'timePicker',
'unifiedSearch',
'lens',
- 'security',
- 'spaceSelector',
'header',
'unifiedFieldList',
]);
@@ -131,16 +129,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
it('should visualize correctly using adhoc data view', async () => {
- await PageObjects.discover.createAdHocDataView('logst', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromSearchBar({
+ name: 'logst',
+ adHoc: true,
+ hasTimeField: true,
+ });
await testSubjects.click('unifiedHistogramEditVisualization');
await PageObjects.header.waitUntilLoadingHasFinished();
- await retry.try(async () => {
- const selectedPattern = await PageObjects.lens.getDataPanelIndexPattern();
- expect(selectedPattern).to.eql('logst*');
- });
+ await dataViews.waitForSwitcherToBe('logst*');
});
it('should visualize correctly text based language queries in Discover', async () => {
diff --git a/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts b/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts
index fc8cbd2b38d66..3ae2ee91b80d2 100644
--- a/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts
+++ b/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts
@@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const browser = getService('browser');
+ const dataViews = getService('dataViews');
const expectedData = [
{ x: '97.220.3.248', y: 19755 },
@@ -48,11 +49,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
await elasticChart.setNewChartUiDebugFlag(true);
- await PageObjects.lens.createAdHocDataView('*stash*');
- retry.try(async () => {
- const selectedPattern = await PageObjects.lens.getDataPanelIndexPattern();
- expect(selectedPattern).to.eql('*stash*');
- });
+ await dataViews.createFromSearchBar({ name: '*stash*', adHoc: true });
+ await dataViews.waitForSwitcherToBe('*stash*');
}
const checkDiscoverNavigationResult = async () => {
@@ -71,7 +69,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const actualDiscoverQueryHits = await testSubjects.getVisibleText('discoverQueryHits');
expect(actualDiscoverQueryHits).to.be('14,005');
- expect(await PageObjects.unifiedSearch.isAdHocDataView()).to.be(true);
+ expect(await dataViews.isAdHoc()).to.be(true);
};
describe('lens ad hoc data view tests', () => {
@@ -96,7 +94,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should allow adding and using a field', async () => {
await PageObjects.lens.switchToVisualization('lnsDatatable');
await retry.try(async () => {
- await PageObjects.lens.clickAddField();
+ await dataViews.clickAddFieldFromSearchBar();
await fieldEditor.setName('runtimefield');
await fieldEditor.enableValue();
await fieldEditor.typeScript("emit('abc')");
@@ -115,9 +113,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should allow switching to another data view and back', async () => {
- await PageObjects.lens.switchDataPanelIndexPattern('logstash-*');
+ await dataViews.switchTo('logstash-*');
await PageObjects.lens.waitForFieldMissing('runtimefield');
- await PageObjects.lens.switchDataPanelIndexPattern('*stash*');
+ await dataViews.switchTo('*stash*');
await PageObjects.lens.waitForField('runtimefield');
});
@@ -220,7 +218,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.unifiedFieldList.clickFieldListItemToggle('_bytes-runtimefield');
const newDataViewId = await PageObjects.discover.getCurrentDataViewId();
expect(newDataViewId).not.to.equal(prevDataViewId);
- expect(await PageObjects.unifiedSearch.isAdHocDataView()).to.be(true);
+ expect(await dataViews.isAdHoc()).to.be(true);
await browser.closeCurrentWindow();
});
diff --git a/x-pack/test/functional/apps/lens/group2/field_formatters.ts b/x-pack/test/functional/apps/lens/group2/field_formatters.ts
index 4b66436fc1c2c..5c52f2bd03acc 100644
--- a/x-pack/test/functional/apps/lens/group2/field_formatters.ts
+++ b/x-pack/test/functional/apps/lens/group2/field_formatters.ts
@@ -10,16 +10,10 @@ import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const PageObjects = getPageObjects([
- 'visualize',
- 'lens',
- 'header',
- 'dashboard',
- 'common',
- 'settings',
- ]);
+ const PageObjects = getPageObjects(['visualize', 'lens', 'header']);
const retry = getService('retry');
const fieldEditor = getService('fieldEditor');
+ const dataViews = getService('dataViews');
describe('lens fields formatters tests', () => {
describe('keyword formatters', () => {
@@ -38,7 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should display url formatter correctly', async () => {
await retry.try(async () => {
- await PageObjects.lens.clickAddField();
+ await dataViews.clickAddFieldFromSearchBar();
await fieldEditor.setName('runtimefield');
await fieldEditor.enableValue();
await fieldEditor.typeScript("emit(doc['geo.dest'].value)");
@@ -137,7 +131,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should display bytes number formatter correctly', async () => {
await retry.try(async () => {
- await PageObjects.lens.clickAddField();
+ await dataViews.clickAddFieldFromSearchBar();
await fieldEditor.setName('runtimefield');
await fieldEditor.setFieldType('long');
await fieldEditor.enableValue();
@@ -211,7 +205,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should be overridden by Lens formatter', async () => {
await retry.try(async () => {
- await PageObjects.lens.clickAddField();
+ await dataViews.clickAddFieldFromSearchBar();
await fieldEditor.setName('runtimefield');
await fieldEditor.setFieldType('long');
await fieldEditor.enableValue();
diff --git a/x-pack/test/functional/apps/lens/group2/fields_list.ts b/x-pack/test/functional/apps/lens/group2/fields_list.ts
index cb29eb74b96d1..50f55eae3c342 100644
--- a/x-pack/test/functional/apps/lens/group2/fields_list.ts
+++ b/x-pack/test/functional/apps/lens/group2/fields_list.ts
@@ -9,14 +9,7 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const PageObjects = getPageObjects([
- 'visualize',
- 'lens',
- 'common',
- 'header',
- 'timePicker',
- 'unifiedFieldList',
- ]);
+ const PageObjects = getPageObjects(['visualize', 'lens', 'timePicker', 'header']);
const find = getService('find');
const log = getService('log');
const testSubjects = getService('testSubjects');
@@ -25,6 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const es = getService('es');
const queryBar = getService('queryBar');
+ const dataViews = getService('dataViews');
describe('lens fields list tests', () => {
for (const datasourceType of ['form-based', 'ad-hoc', 'ad-hoc-no-timefield']) {
@@ -34,14 +28,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visualize.clickVisType('lens');
if (datasourceType !== 'form-based') {
- await PageObjects.lens.createAdHocDataView(
- '*stash*',
- datasourceType !== 'ad-hoc-no-timefield'
- );
- retry.try(async () => {
- const selectedPattern = await PageObjects.lens.getDataPanelIndexPattern();
- expect(selectedPattern).to.eql('*stash*');
+ await dataViews.createFromSearchBar({
+ name: '*stash*',
+ adHoc: true,
+ hasTimeField: datasourceType !== 'ad-hoc-no-timefield',
});
+ await dataViews.waitForSwitcherToBe('*stash*');
}
if (datasourceType !== 'ad-hoc-no-timefield') {
@@ -49,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
}
await retry.try(async () => {
- await PageObjects.lens.clickAddField();
+ await dataViews.clickAddFieldFromSearchBar();
await fieldEditor.setName('runtime_string');
await fieldEditor.enableValue();
await fieldEditor.typeScript("emit('abc')");
@@ -254,11 +246,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visualize.clickVisType('lens');
await PageObjects.timePicker.setCommonlyUsedTime('This_week');
- await PageObjects.lens.createAdHocDataView('field-update-test', true);
- await retry.try(async () => {
- const selectedPattern = await PageObjects.lens.getDataPanelIndexPattern();
- expect(selectedPattern).to.eql('field-update-test*');
+ await dataViews.createFromSearchBar({
+ name: 'field-update-test',
+ adHoc: true,
+ hasTimeField: true,
});
+ await dataViews.waitForSwitcherToBe('field-update-test*');
});
after(async () => {
await es.transport.request({
diff --git a/x-pack/test/functional/apps/lens/group2/persistent_context.ts b/x-pack/test/functional/apps/lens/group2/persistent_context.ts
index de34f71272427..5f82c0aab22a4 100644
--- a/x-pack/test/functional/apps/lens/group2/persistent_context.ts
+++ b/x-pack/test/functional/apps/lens/group2/persistent_context.ts
@@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const security = getService('security');
const listingTable = getService('listingTable');
const queryBar = getService('queryBar');
+ const dataViews = getService('dataViews');
describe('lens query context', () => {
before(async () => {
@@ -200,9 +201,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.lens.switchDataPanelIndexPattern('log*');
await browser.refresh();
// Lens app can take a while to be fully functional after refresh, retry assertion
- await retry.try(async () => {
- expect(await PageObjects.lens.getDataPanelIndexPattern()).to.equal('log*');
- });
+ await dataViews.waitForSwitcherToBe('log*');
});
it('keeps time range and pinned filters after refreshing directly after saving', async () => {
diff --git a/x-pack/test/functional/apps/lens/group3/runtime_fields.ts b/x-pack/test/functional/apps/lens/group3/runtime_fields.ts
index 5031350b588ab..8803a14ea34fe 100644
--- a/x-pack/test/functional/apps/lens/group3/runtime_fields.ts
+++ b/x-pack/test/functional/apps/lens/group3/runtime_fields.ts
@@ -9,10 +9,11 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']);
+ const PageObjects = getPageObjects(['visualize', 'lens', 'header']);
const filterBar = getService('filterBar');
const fieldEditor = getService('fieldEditor');
const retry = getService('retry');
+ const dataViews = getService('dataViews');
describe('lens runtime fields', () => {
it('should be able to add runtime field and use it', async () => {
@@ -21,7 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.lens.goToTimeRange();
await PageObjects.lens.switchToVisualization('lnsDatatable');
await retry.try(async () => {
- await PageObjects.lens.clickAddField();
+ await dataViews.clickAddFieldFromSearchBar();
await fieldEditor.setName('runtimefield');
await fieldEditor.enableValue();
await fieldEditor.typeScript("emit('abc')");
diff --git a/x-pack/test/functional/apps/lens/group5/formula.ts b/x-pack/test/functional/apps/lens/group5/formula.ts
index d1ea07feb9c70..e02548d7108ca 100644
--- a/x-pack/test/functional/apps/lens/group5/formula.ts
+++ b/x-pack/test/functional/apps/lens/group5/formula.ts
@@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const fieldEditor = getService('fieldEditor');
const retry = getService('retry');
+ const dataViews = getService('dataViews');
describe('lens formula', () => {
it('should transition from count to formula', async () => {
@@ -97,7 +98,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visualize.clickVisType('lens');
await PageObjects.lens.goToTimeRange();
await PageObjects.lens.switchToVisualization('lnsDatatable');
- await PageObjects.lens.clickAddField();
+ await dataViews.clickAddFieldFromSearchBar();
await fieldEditor.setName(`ab' "'`, true, true);
await fieldEditor.enableValue();
await fieldEditor.typeScript("emit('abc')");
diff --git a/x-pack/test/functional/apps/lens/group6/no_data.ts b/x-pack/test/functional/apps/lens/group6/no_data.ts
index 2438f6d60824f..04d7967985da3 100644
--- a/x-pack/test/functional/apps/lens/group6/no_data.ts
+++ b/x-pack/test/functional/apps/lens/group6/no_data.ts
@@ -8,22 +8,13 @@
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const retry = getService('retry');
- const find = getService('find');
+ const dataViews = getService('dataViews');
const esArchiver = getService('esArchiver');
const es = getService('es');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common', 'lens', 'header', 'timePicker']);
- const createDataView = async (dataViewName: string) => {
- await testSubjects.setValue('createIndexPatternTitleInput', dataViewName, {
- clearWithKeyboard: true,
- typeCharByChar: true,
- });
- await testSubjects.click('saveIndexPatternButton');
- };
-
describe('lens no data', () => {
before(async function () {
// delete all non-hidden indices to make it really "no data"
@@ -51,25 +42,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.savedObjects.clean({ types: ['index-pattern'] });
await PageObjects.common.navigateToApp('lens');
- const button = await testSubjects.find('createDataViewButton');
- button.click();
- await retry.waitForWithTimeout('index pattern editor form to be visible', 15000, async () => {
- return await (await find.byClassName('indexPatternEditor__form')).isDisplayed();
- });
-
const dataViewToCreate = 'logstash';
- await createDataView(dataViewToCreate);
- await PageObjects.header.waitUntilLoadingHasFinished();
- await retry.waitForWithTimeout(
- 'data view selector to include a newly created dataview',
- 5000,
- async () => {
- const dataViewTitle = await PageObjects.lens.getDataPanelIndexPattern();
- // data view editor will add wildcard symbol by default
- // so we need to include it in our original title when comparing
- return dataViewTitle === `${dataViewToCreate}*`;
- }
- );
+ await dataViews.createFromPrompt({ name: dataViewToCreate });
+ await dataViews.waitForSwitcherToBe(`${dataViewToCreate}*`);
});
});
}
diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_filters.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_filters.ts
index 4be111e00bc85..a677b467d4768 100644
--- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_filters.ts
+++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_filters.ts
@@ -23,6 +23,7 @@ const PINNED_FILTER = {
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings', 'header']);
const filterBar = getService('filterBar');
@@ -36,7 +37,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it(`retains pinned filters from other plugins`, async () => {
await ml.navigation.navigateToDiscoverViaAppsMenu();
- await ml.dashboardEmbeddables.selectDiscoverIndexPattern('ft_farequote');
+
+ await dataViews.switchToAndValidate('ft_farequote');
await PageObjects.timePicker.setAbsoluteRange(startTime, endTime);
await filterBar.addFilter({
diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_dashboard.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_dashboard.ts
index 7d38012c3651b..3f7e726543485 100644
--- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_dashboard.ts
+++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_dashboard.ts
@@ -21,6 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'header',
]);
const ml = getService('ml');
+ const dataViews = getService('dataViews');
const retry = getService('retry');
const dashboardAddPanel = getService('dashboardAddPanel');
@@ -45,9 +46,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch);
});
} else {
- await ml.dashboardEmbeddables.selectDiscoverIndexPattern(
- testData.sourceIndexOrSavedSearch
- );
+ await dataViews.switchToAndValidate(testData.sourceIndexOrSavedSearch);
}
await PageObjects.timePicker.setAbsoluteRange(startTime, endTime);
diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover.ts
index 34a8d59cc2147..7c1e2a1a8f946 100644
--- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover.ts
+++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover.ts
@@ -23,6 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings']);
const ml = getService('ml');
const retry = getService('retry');
+ const dataViews = getService('dataViews');
const startTime = 'Jan 1, 2016 @ 00:00:00.000';
const endTime = 'Nov 1, 2020 @ 00:00:00.000';
@@ -35,7 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch);
});
} else {
- await ml.dashboardEmbeddables.selectDiscoverIndexPattern(testData.sourceIndexOrSavedSearch);
+ await dataViews.switchToAndValidate(testData.sourceIndexOrSavedSearch);
}
await PageObjects.timePicker.setAbsoluteRange(startTime, endTime);
@@ -54,9 +55,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch);
});
} else {
- await ml.dashboardEmbeddables.selectDiscoverIndexPattern(
- testData.sourceIndexOrSavedSearch
- );
+ await dataViews.switchToAndValidate(testData.sourceIndexOrSavedSearch);
}
await PageObjects.timePicker.setAbsoluteRange(startTime, endTime);
diff --git a/x-pack/test/functional/apps/observability_logs_explorer/data_source_selector.ts b/x-pack/test/functional/apps/observability_logs_explorer/data_source_selector.ts
index aa68e4af689bd..100d547cdb07d 100644
--- a/x-pack/test/functional/apps/observability_logs_explorer/data_source_selector.ts
+++ b/x-pack/test/functional/apps/observability_logs_explorer/data_source_selector.ts
@@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects(['common', 'discover', 'observabilityLogsExplorer']);
const noIntegrationsTitle = 'No integrations found';
@@ -772,11 +773,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(url).to.contain(`/app/discover`);
});
- await retry.try(async () => {
- expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.eql(
- expectedDataViews[2]
- );
- });
+ await dataViews.waitForSwitcherToBe(expectedDataViews[2]);
});
});
diff --git a/x-pack/test/functional/apps/observability_logs_explorer/header_menu.ts b/x-pack/test/functional/apps/observability_logs_explorer/header_menu.ts
index f7c45395dd758..77fa726a7c235 100644
--- a/x-pack/test/functional/apps/observability_logs_explorer/header_menu.ts
+++ b/x-pack/test/functional/apps/observability_logs_explorer/header_menu.ts
@@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
const testSubjects = getService('testSubjects');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects(['discover', 'observabilityLogsExplorer', 'timePicker']);
describe('Header menu', () => {
@@ -62,9 +63,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitForDocTableLoadingComplete();
- await retry.try(async () => {
- expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.eql('All logs');
- });
+ await dataViews.waitForSwitcherToBe('All logs');
await retry.try(async () => {
expect(await PageObjects.discover.getColumnHeaders()).to.eql([
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index 43e238acf6ecb..c82ac1a72ec81 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -1043,13 +1043,6 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await PageObjects.header.waitUntilLoadingHasFinished();
},
- /**
- * Returns the current index pattern of the data panel
- */
- async getDataPanelIndexPattern() {
- return await PageObjects.unifiedSearch.getSelectedDataView('lns-dataView-switch-link');
- },
-
/**
* Returns the current index pattern of the first layer
*/
@@ -1487,17 +1480,6 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
});
},
- async clickAddField() {
- await testSubjects.click('lns-dataView-switch-link');
- await testSubjects.existOrFail('indexPattern-add-field');
- await testSubjects.click('indexPattern-add-field');
- },
-
- async createAdHocDataView(name: string, hasTimeField?: boolean) {
- await testSubjects.click('lns-dataView-switch-link');
- await PageObjects.unifiedSearch.createNewDataView(name, true, hasTimeField);
- },
-
async switchToTextBasedLanguage(language: string) {
await testSubjects.click('lns-dataView-switch-link');
await PageObjects.unifiedSearch.selectTextBasedLanguage(language);
diff --git a/x-pack/test/functional/services/ml/dashboard_embeddables.ts b/x-pack/test/functional/services/ml/dashboard_embeddables.ts
index 587a5762f4487..7ebd7b9e74e47 100644
--- a/x-pack/test/functional/services/ml/dashboard_embeddables.ts
+++ b/x-pack/test/functional/services/ml/dashboard_embeddables.ts
@@ -10,15 +10,13 @@ import { FtrProviderContext } from '../../ftr_provider_context';
import { MlDashboardJobSelectionTable } from './dashboard_job_selection_table';
export function MachineLearningDashboardEmbeddablesProvider(
- { getService, getPageObjects }: FtrProviderContext,
+ { getService }: FtrProviderContext,
mlDashboardJobSelectionTable: MlDashboardJobSelectionTable
) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const find = getService('find');
- // const ml = getService('ml');
const dashboardAddPanel = getService('dashboardAddPanel');
- const PageObjects = getPageObjects(['discover']);
return {
async assertAnomalyChartsEmbeddableInitializerExists() {
@@ -137,15 +135,5 @@ export function MachineLearningDashboardEmbeddablesProvider(
await mlDashboardJobSelectionTable.assertJobSelectionTableExists();
});
},
-
- async selectDiscoverIndexPattern(indexPattern: string) {
- await retry.tryForTime(2 * 1000, async () => {
- await PageObjects.discover.selectIndexPattern(indexPattern);
- const indexPatternTitle = await testSubjects.getVisibleText(
- 'discover-dataView-switch-link'
- );
- expect(indexPatternTitle).to.be(indexPattern);
- });
- },
};
}
diff --git a/x-pack/test/functional/services/transform/discover.ts b/x-pack/test/functional/services/transform/discover.ts
index 02cf7a98310eb..793291689852a 100644
--- a/x-pack/test/functional/services/transform/discover.ts
+++ b/x-pack/test/functional/services/transform/discover.ts
@@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export function TransformDiscoverProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
+ const dataViews = getService('dataViews');
return {
async assertDiscoverQueryHits(expectedDiscoverQueryHits: string) {
@@ -37,9 +38,8 @@ export function TransformDiscoverProvider({ getService }: FtrProviderContext) {
await testSubjects.missingOrFail('discoverQueryHits');
// Discover should use the destination index pattern
- const actualIndexPatternSwitchLinkText = await (
- await testSubjects.find('discover-dataView-switch-link')
- ).getVisibleText();
+ const actualIndexPatternSwitchLinkText = await dataViews.getSelectedName();
+
expect(actualIndexPatternSwitchLinkText).to.eql(
expectedDestinationIndex,
`Destination index should be ${expectedDestinationIndex}, got ${actualIndexPatternSwitchLinkText}`
diff --git a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts
index ec192185829b5..c04cf14322ef4 100644
--- a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts
@@ -13,14 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const es = getService('es');
const monacoEditor = getService('monacoEditor');
- const PageObjects = getPageObjects([
- 'settings',
- 'common',
- 'header',
- 'discover',
- 'timePicker',
- 'dashboard',
- ]);
+ const PageObjects = getPageObjects(['settings', 'common', 'header', 'discover', 'timePicker']);
const deployment = getService('deployment');
const dataGrid = getService('dataGrid');
const browser = getService('browser');
@@ -33,6 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const find = getService('find');
const toasts = getService('toasts');
const kibanaServer = getService('kibanaServer');
+ const dataViews = getService('dataViews');
const SOURCE_DATA_VIEW = 'search-source-alert';
const OUTPUT_DATA_VIEW = 'search-source-alert-output';
@@ -210,7 +204,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.clickNewSearchButton(); // reset params
- await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_VIEW);
+ await dataViews.switchToAndValidate(OUTPUT_DATA_VIEW);
let ruleId: string;
if (type === 'name') {
@@ -263,7 +257,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
}
expect(await filterBar.getFilterCount()).to.be(0);
expect(await queryBar.getQueryString()).to.equal('');
- const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
+ const selectedDataView = await dataViews.getSelectedName();
const { valid } = await PageObjects.discover.validateDataViewReffsEquality();
expect(valid).to.equal(true);
expect(selectedDataView).to.be.equal(dataView);
@@ -350,7 +344,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should show time field validation error', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.discover.waitUntilSearchingHasFinished();
- await PageObjects.discover.selectIndexPattern(SOURCE_DATA_VIEW);
+ await dataViews.switchToAndValidate(SOURCE_DATA_VIEW);
await PageObjects.timePicker.setCommonlyUsedTime('Last_15 minutes');
await openDiscoverAlertFlyout();
@@ -424,7 +418,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should display actual state after rule params update on clicking viewInApp link', async () => {
await clickViewInApp(RULE_NAME);
- const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
+ const selectedDataView = await dataViews.getSelectedName();
expect(selectedDataView).to.be.equal(SOURCE_DATA_VIEW);
await checkUpdatedRuleParamsState();
@@ -470,8 +464,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should navigate to alert results via link provided in notification using adhoc data view', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.discover.waitUntilSearchingHasFinished();
- await PageObjects.discover.createAdHocDataView('search-source-', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromSearchBar({
+ name: 'search-source-',
+ adHoc: true,
+ hasTimeField: true,
+ });
await PageObjects.timePicker.setCommonlyUsedTime('Last_15 minutes');
await PageObjects.discover.addRuntimeField('runtime-message-field', `emit('mock-message')`);
@@ -484,7 +481,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await openAlertResults(ADHOC_RULE_NAME);
- const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
+ const selectedDataView = await dataViews.getSelectedName();
expect(selectedDataView).to.be.equal('search-source-*');
const documentCell = await dataGrid.getCellElement(0, 3);
@@ -498,7 +495,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// navigate to discover using view in app link
await clickViewInApp(ADHOC_RULE_NAME);
- const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
+ const selectedDataView = await dataViews.getSelectedName();
expect(selectedDataView).to.be.equal('search-source-*');
const documentCell = await dataGrid.getCellElement(0, 3);
@@ -507,7 +504,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should display results after data view removal on clicking prev generated link', async () => {
- await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_VIEW);
+ await dataViews.switchToAndValidate(OUTPUT_DATA_VIEW);
await deleteDataView(sourceDataViewId);
await openAlertResults(RULE_NAME);
@@ -527,7 +524,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should display results after rule removal on following generated link', async () => {
- await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_VIEW);
+ await dataViews.switchToAndValidate(OUTPUT_DATA_VIEW);
const [{ id: firstAlertId }] = await getAlertsByName(RULE_NAME);
await deleteAlerts([firstAlertId]);
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 32349e5919847..0bbae5c25cdd5 100644
--- a/x-pack/test_serverless/functional/services/deployment_agnostic_services.ts
+++ b/x-pack/test_serverless/functional/services/deployment_agnostic_services.ts
@@ -37,6 +37,7 @@ const deploymentAgnosticFunctionalServices = _.pick(functionalServices, [
'dataGrid',
'dataStreams',
'docTable',
+ 'dataViews',
'elasticChart',
'embedding',
'failureDebugging',
diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group2/_adhoc_data_views.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group2/_adhoc_data_views.ts
index 192014d40f2b8..dd9494c24ea0f 100644
--- a/x-pack/test_serverless/functional/test_suites/common/discover/group2/_adhoc_data_views.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/discover/group2/_adhoc_data_views.ts
@@ -32,6 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'svlCommonNavigation',
]);
const security = getService('security');
+ const dataViews = getService('dataViews');
const addSearchToDashboard = async (name: string) => {
await dashboardAddPanel.addSavedSearch(name);
@@ -55,8 +56,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should navigate back correctly from to surrounding and single views', async () => {
- await PageObjects.discover.createAdHocDataView('logstash', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromSearchBar({
+ name: 'logstash',
+ adHoc: true,
+ hasTimeField: true,
+ });
const first = await PageObjects.discover.getCurrentDataViewId();
await PageObjects.discover.addRuntimeField(
@@ -77,7 +81,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.svlCommonNavigation.breadcrumbs.clickBreadcrumb({ deepLinkId: 'discover' });
await PageObjects.header.waitUntilLoadingHasFinished();
- expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.be('logstash*');
+ expect(await dataViews.getSelectedName()).to.be('logstash*');
// navigate to single doc view
await dataGrid.clickRowToggle({ rowIndex: 0 });
@@ -89,7 +93,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.svlCommonNavigation.breadcrumbs.clickBreadcrumb({ deepLinkId: 'discover' });
await PageObjects.header.waitUntilLoadingHasFinished();
- expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.be('logstash*');
+ expect(await dataViews.getSelectedName()).to.be('logstash*');
});
it('should support query and filtering', async () => {
@@ -135,8 +139,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('search results should be different after data view update', async () => {
- await PageObjects.discover.createAdHocDataView('logst', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromSearchBar({
+ name: 'logst',
+ adHoc: true,
+ hasTimeField: true,
+ });
const prevDataViewId = await PageObjects.discover.getCurrentDataViewId();
// trigger data view id update
@@ -236,8 +243,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should notify about invalid filter reffs', async () => {
- await PageObjects.discover.createAdHocDataView('logstas', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromSearchBar({
+ name: 'logstas',
+ adHoc: true,
+ hasTimeField: true,
+ });
await filterBar.addFilter({
field: 'nestedField.child',
diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts
index e69dcb361722d..78ab4b43c4ba2 100644
--- a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts
@@ -23,6 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const filterBar = getService('filterBar');
const queryBar = getService('queryBar');
const elasticChart = getService('elasticChart');
+ const dataViews = getService('dataViews');
describe('discover request counts', function describeIndexTests() {
before(async function () {
@@ -216,7 +217,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should send 2 requests (documents + chart) when changing the data view', async () => {
await expectSearches(type, 2, async () => {
- await PageObjects.discover.selectIndexPattern('long-window-logstash-*');
+ await dataViews.switchToAndValidate('long-window-logstash-*');
});
});
});
diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts
index 31262b8a24262..54f7aa6339702 100644
--- a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts
@@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const fieldEditor = getService('fieldEditor');
const retry = getService('retry');
const dataGrid = getService('dataGrid');
+ const dataViews = getService('dataViews');
const INITIAL_FIELD_LIST_SUMMARY = '48 available fields. 5 empty fields. 3 meta fields.';
describe('discover sidebar', function describeIndexTests() {
@@ -393,7 +394,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
INITIAL_FIELD_LIST_SUMMARY
);
- await PageObjects.discover.selectIndexPattern('with-timefield');
+ await dataViews.switchToAndValidate('with-timefield');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -410,7 +411,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
)}NoFieldsCallout-noFieldsExist`
);
- await PageObjects.discover.selectIndexPattern('logstash-*');
+ await dataViews.switchToAndValidate('logstash-*');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -438,7 +439,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
INITIAL_FIELD_LIST_SUMMARY
);
- await PageObjects.discover.selectIndexPattern('without-timefield');
+ await dataViews.switchToAndValidate('without-timefield');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -447,7 +448,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'6 available fields. 3 meta fields.'
);
- await PageObjects.discover.selectIndexPattern('with-timefield');
+ await dataViews.switchToAndValidate('with-timefield');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -461,7 +462,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
)}NoFieldsCallout-noFieldsMatch`
);
- await PageObjects.discover.selectIndexPattern('logstash-*');
+ await dataViews.switchToAndValidate('logstash-*');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -517,7 +518,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
INITIAL_FIELD_LIST_SUMMARY
);
- await PageObjects.discover.selectIndexPattern('indices-stats*');
+ await dataViews.switchToAndValidate('indices-stats*');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -526,7 +527,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'6873 available fields. 3 meta fields.'
);
- await PageObjects.discover.selectIndexPattern('logstash-*');
+ await dataViews.switchToAndValidate('logstash-*');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -542,8 +543,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should work with ad-hoc data views and runtime fields', async () => {
- await PageObjects.discover.createAdHocDataView('logstash', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromSearchBar({
+ name: 'logstash',
+ adHoc: true,
+ hasTimeField: true,
+ });
expect(await PageObjects.unifiedFieldList.getSidebarAriaDescription()).to.be(
INITIAL_FIELD_LIST_SUMMARY
@@ -652,7 +656,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
INITIAL_FIELD_LIST_SUMMARY
);
- await PageObjects.discover.selectIndexPattern('with-timefield');
+ await dataViews.switchToAndValidate('with-timefield');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/visualize_field.ts b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/visualize_field.ts
index 29bba81403322..42d1ad33b30f0 100644
--- a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/visualize_field.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/visualize_field.ts
@@ -16,6 +16,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const kibanaServer = getService('kibanaServer');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects([
'common',
'svlCommonPage',
@@ -119,16 +120,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
it('should visualize correctly using adhoc data view', async () => {
- await PageObjects.discover.createAdHocDataView('logst', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromSearchBar({
+ name: 'logst',
+ adHoc: true,
+ hasTimeField: true,
+ });
await testSubjects.click('unifiedHistogramEditVisualization');
await PageObjects.header.waitUntilLoadingHasFinished();
-
- await retry.try(async () => {
- const selectedPattern = await PageObjects.lens.getDataPanelIndexPattern();
- expect(selectedPattern).to.eql('logst*');
- });
+ await dataViews.waitForSwitcherToBe('logst*');
});
// TODO: ES|QL tests removed since ES|QL isn't supported in Serverless
diff --git a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts
index c09d9a8e6836f..f1f71971c41ae 100644
--- a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts
@@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const toasts = getService('toasts');
const kibanaServer = getService('kibanaServer');
const comboBox = getService('comboBox');
+ const dataViews = getService('dataViews');
const SOURCE_DATA_VIEW = 'search-source-alert';
const OUTPUT_DATA_VIEW = 'search-source-alert-output';
@@ -242,8 +243,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.clickNewSearchButton(); // reset params
-
- await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_VIEW);
+ await dataViews.switchToAndValidate(OUTPUT_DATA_VIEW);
let ruleId: string;
if (type === 'name') {
@@ -299,7 +299,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
}
expect(await filterBar.getFilterCount()).to.be(0);
expect(await queryBar.getQueryString()).to.equal('');
- const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
+
+ const selectedDataView = await dataViews.getSelectedName();
const { valid } = await PageObjects.discover.validateDataViewReffsEquality();
expect(valid).to.equal(true);
expect(selectedDataView).to.be.equal(dataView);
@@ -401,7 +402,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should show time field validation error', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.discover.waitUntilSearchingHasFinished();
- await PageObjects.discover.selectIndexPattern(SOURCE_DATA_VIEW);
+ await dataViews.switchToAndValidate(SOURCE_DATA_VIEW);
await PageObjects.timePicker.setCommonlyUsedTime('Last_15 minutes');
await openDiscoverAlertFlyout();
@@ -475,7 +476,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should display actual state after rule params update on clicking viewInApp link', async () => {
await clickViewInApp(RULE_NAME);
- const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
+ const selectedDataView = await dataViews.getSelectedName();
expect(selectedDataView).to.be.equal(SOURCE_DATA_VIEW);
await checkUpdatedRuleParamsState();
@@ -521,8 +522,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should navigate to alert results via link provided in notification using adhoc data view', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.discover.waitUntilSearchingHasFinished();
- await PageObjects.discover.createAdHocDataView('search-source-', true);
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await dataViews.createFromSearchBar({
+ name: 'search-source-',
+ adHoc: true,
+ hasTimeField: true,
+ });
await PageObjects.timePicker.setCommonlyUsedTime('Last_15 minutes');
await PageObjects.discover.addRuntimeField('runtime-message-field', `emit('mock-message')`);
@@ -535,7 +539,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await openAlertResults(ADHOC_RULE_NAME);
- const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
+ const selectedDataView = await dataViews.getSelectedName();
expect(selectedDataView).to.be.equal('search-source-*');
const documentCell = await dataGrid.getCellElement(0, 3);
@@ -549,7 +553,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// navigate to discover using view in app link
await clickViewInApp(ADHOC_RULE_NAME);
- const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
+ const selectedDataView = await dataViews.getSelectedName();
expect(selectedDataView).to.be.equal('search-source-*');
const documentCell = await dataGrid.getCellElement(0, 3);
@@ -558,7 +562,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should display results after data view removal on clicking prev generated link', async () => {
- await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_VIEW);
+ await dataViews.switchToAndValidate(OUTPUT_DATA_VIEW);
await deleteDataView(sourceDataViewId);
await openAlertResults(RULE_NAME);
@@ -578,7 +582,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should display results after rule removal on following generated link', async () => {
- await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_VIEW);
+ await dataViews.switchToAndValidate(OUTPUT_DATA_VIEW);
const [{ id: firstAlertId }] = await getAlertsByName(RULE_NAME);
await deleteAlerts([firstAlertId]);
diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts
index 9140645df8094..df749f2d4f9dd 100644
--- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts
+++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts
@@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const esArchiver = getService('esArchiver');
const retry = getService('retry');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects([
'common',
'discover',
@@ -717,11 +718,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(url).to.contain(`/app/discover`);
});
- await retry.try(async () => {
- expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.eql(
- expectedDataViews[1]
- );
- });
+ await dataViews.waitForSwitcherToBe(expectedDataViews[1]);
});
});
diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/header_menu.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/header_menu.ts
index e6fe60cf248fc..23d2e7729e2f4 100644
--- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/header_menu.ts
+++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/header_menu.ts
@@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
const testSubjects = getService('testSubjects');
+ const dataViews = getService('dataViews');
const PageObjects = getPageObjects([
'discover',
'observabilityLogsExplorer',
@@ -85,9 +86,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitForDocTableLoadingComplete();
- await retry.try(async () => {
- expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.eql('All logs');
- });
+ await dataViews.waitForSwitcherToBe('All logs');
+
await retry.try(async () => {
expect(await PageObjects.discover.getColumnHeaders()).to.eql([
'@timestamp',
@@ -145,9 +145,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
expect(await browser.getCurrentUrl()).contain('/app/discover');
- await retry.try(async () => {
- expect(await PageObjects.discover.getCurrentlySelectedDataView()).not.to.eql('All logs');
- });
+ await dataViews.waitForSwitcherToBe('All logs');
await retry.try(async () => {
expect(await PageObjects.discover.getColumnHeaders()).not.to.eql([
From b328a5e6e81a3aa3ded5540019bdbd592462b03a Mon Sep 17 00:00:00 2001
From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
Date: Wed, 10 Apr 2024 16:35:03 +0200
Subject: [PATCH 13/55] [Fleet] added Fleet synthetic check to staging quality
gates (#180461)
## Summary
Closes https://github.com/elastic/ingest-dev/issues/3065
Added Fleet synthetic monitor check to Kibana staging quality gates.
It has been stable in the past two weeks, added with soft fail for now.
This monitor verifies that a long running project is healthy in staging.
It aims to flag issues if there is a breaking change in Kibana / Fleet
plugin.
---
.../quality-gates/pipeline.tests-staging.yaml | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml
index 2b74ee24165dc..837234fc51441 100644
--- a/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml
+++ b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml
@@ -21,6 +21,19 @@ steps:
EC_REGION: aws-us-east-1
RETRY_TESTS_ON_FAIL: "true"
message: "${BUILDKITE_MESSAGE} (triggered by pipeline.tests-staging.yaml)"
+
+ - label: ":rocket: Fleet synthetic monitor to check the long standing project"
+ trigger: "serverless-quality-gates"
+ build:
+ message: "${BUILDKITE_MESSAGE} (triggered by pipeline.tests-staging.yaml)"
+ env:
+ TARGET_ENV: staging
+ CHECK_SYNTHETICS: true
+ CHECK_SYNTHETICS_TAG: "fleet"
+ CHECK_SYNTHETICS_MINIMUM_RUNS: 3
+ MAX_FAILURES: 2
+ CHECK_SYNTHETIC_MAX_POLL: 50
+ soft_fail: true
- wait: ~
From d45157ba496baa290e39070951c5dffded9a732a Mon Sep 17 00:00:00 2001
From: Marshall Main <55718608+marshallmain@users.noreply.github.com>
Date: Wed, 10 Apr 2024 07:35:21 -0700
Subject: [PATCH 14/55] [Security Solution] Classify EQL verification and ML
job missing errors as user errors (#180094)
## Summary
Building on https://github.com/elastic/kibana/pull/180040
Detection rules commonly fail when prebuilt rules are imported and
enabled without the appropriate indices (for EQL) or ML jobs (for ML
rules). EQL rules fail with a `verification_exception` because the EQL
search API validates the fields in the search request against the
indices in the request; if there are no indices then it returns an
exception. ML rules fail with a ` missing` exception on the
search request if the job is not found. Both of these errors do not mean
that the system is overloaded or performing incorrectly in some way, but
they are still showing up in large volumes on SLO dashboards.
This PR builds on #180040, which introduces the ability to classify
errors as "user errors" when the error is not due to some kind of system
malfunction, but more related to incorrect (or insufficient) user
actions.
### Testing
#### EQL
1. Create 2 indices, `test` and `test2`
```
PUT /test
{
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
}
}
}
}
PUT /test2
{
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"event.category": {
"type": "keyword"
}
}
}
}
```
2. Create (disabled) an EQL rule that queries `test*` and uses a simple
query like `file where true`
3. Delete the index `test2`
4. Enable the rule. The rule will fail with a `verification_exception`
because `test` does not have `event.category`.
5. Use your favorite debugging method to verify that `userError` was
`true` in `addLastRunError` in
`x-pack/plugins/alerting/server/monitoring/rule_result_service.ts`
(hopefully this property will be added to the rule SO so we can check it
there in API integration tests)
#### ML rules
1. Import a prebuilt ML rule (`Unusual Process Spawned by a User`, for
example)
2. Enable the rule. The rule will fail with `An error occurred during
rule execution: message: "problem_child_rare_process_by_user missing"`
3. Use your favorite debugging method to verify that `userError` was
`true` in `addLastRunError` in
`x-pack/plugins/alerting/server/monitoring/rule_result_service.ts`
(hopefully this property will be added to the rule SO so we can check it
there in API integration tests)
---
.../client_for_executors/client.ts | 8 +-
.../client_for_executors/client_interface.ts | 1 +
.../create_security_rule_type_wrapper.ts | 2 +
.../rule_types/eql/eql.test.ts | 24 ++++++
.../detection_engine/rule_types/eql/eql.ts | 74 +++++++++++--------
.../detection_engine/rule_types/ml/ml.test.ts | 23 ++++++
.../lib/detection_engine/rule_types/ml/ml.ts | 39 ++++++----
.../lib/detection_engine/rule_types/types.ts | 2 +
.../execution_logic/eql.ts | 46 +++++++++++-
.../execution_logic/machine_learning.ts | 46 +++++++++++-
10 files changed, 217 insertions(+), 48 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts
index 8e9a2970f5dbf..51e8f479bbc1f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts
@@ -164,7 +164,7 @@ export const createRuleExecutionLogClientForExecutors = (
};
const writeStatusChangeToRuleObject = async (args: NormalizedStatusChangeArgs): Promise => {
- const { newStatus, message, metrics } = args;
+ const { newStatus, message, metrics, userError } = args;
if (newStatus === RuleExecutionStatusEnum.running) {
return;
@@ -189,7 +189,7 @@ export const createRuleExecutionLogClientForExecutors = (
}
if (newStatus === RuleExecutionStatusEnum.failed) {
- ruleResultService.addLastRunError(message);
+ ruleResultService.addLastRunError(message, userError ?? false);
} else if (newStatus === RuleExecutionStatusEnum['partial failure']) {
ruleResultService.addLastRunWarning(message);
}
@@ -233,6 +233,7 @@ interface NormalizedStatusChangeArgs {
newStatus: RuleExecutionStatus;
message: string;
metrics?: RuleExecutionMetrics;
+ userError?: boolean;
}
const normalizeStatusChangeArgs = (args: StatusChangeArgs): NormalizedStatusChangeArgs => {
@@ -242,7 +243,7 @@ const normalizeStatusChangeArgs = (args: StatusChangeArgs): NormalizedStatusChan
message: '',
};
}
- const { newStatus, message, metrics } = args;
+ const { newStatus, message, metrics, userError } = args;
return {
newStatus,
@@ -255,6 +256,7 @@ const normalizeStatusChangeArgs = (args: StatusChangeArgs): NormalizedStatusChan
execution_gap_duration_s: normalizeGap(metrics.executionGap),
}
: undefined,
+ userError,
};
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts
index c6a133dce01d7..2200424891fee 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts
@@ -122,6 +122,7 @@ export interface StatusChangeArgs {
newStatus: RuleExecutionStatus;
message?: string;
metrics?: MetricsArgs;
+ userError?: boolean;
}
export interface MetricsArgs {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts
index b36d22e505d27..4577b83540e5b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts
@@ -440,6 +440,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
success: result.success && runResult.success,
warning: warningMessages.length > 0,
warningMessages,
+ userError: runResult.userError,
};
runState = runResult.state;
}
@@ -516,6 +517,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
indexingDurations: result.bulkCreateTimes,
enrichmentDurations: result.enrichmentTimes,
},
+ userError: result.userError,
});
}
} catch (error) {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts
index fb0920bef9ade..9884cd99f4770 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts
@@ -66,5 +66,29 @@ describe('eql_executor', () => {
}`,
]);
});
+
+ it('should classify EQL verification exceptions as "user errors" when reporting to the framework', async () => {
+ alertServices.scopedClusterClient.asCurrentUser.eql.search.mockRejectedValue({
+ name: 'ResponseError',
+ message:
+ 'verification_exception\n\tRoot causes:\n\t\tverification_exception: Found 1 problem\nline 1:1: Unknown column [event.category]',
+ });
+ const result = await eqlExecutor({
+ inputIndex: DEFAULT_INDEX_PATTERN,
+ runtimeMappings: {},
+ completeRule: eqlCompleteRule,
+ tuple,
+ ruleExecutionLogger,
+ services: alertServices,
+ version,
+ bulkCreate: jest.fn(),
+ wrapHits: jest.fn(),
+ wrapSequences: jest.fn(),
+ primaryTimestamp: '@timestamp',
+ exceptionFilter: undefined,
+ unprocessedExceptions: [],
+ });
+ expect(result.userError).toEqual(true);
+ });
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts
index 475a7f6dcdafe..4f5caa3f96f4a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts
@@ -100,40 +100,54 @@ export const eqlExecutor = async ({
}
const eqlSignalSearchStart = performance.now();
- const response = await services.scopedClusterClient.asCurrentUser.eql.search(
- request
- );
+ try {
+ const response = await services.scopedClusterClient.asCurrentUser.eql.search(
+ request
+ );
- const eqlSignalSearchEnd = performance.now();
- const eqlSearchDuration = makeFloatString(eqlSignalSearchEnd - eqlSignalSearchStart);
- result.searchAfterTimes = [eqlSearchDuration];
+ const eqlSignalSearchEnd = performance.now();
+ const eqlSearchDuration = makeFloatString(eqlSignalSearchEnd - eqlSignalSearchStart);
+ result.searchAfterTimes = [eqlSearchDuration];
- let newSignals: Array> | undefined;
- if (response.hits.sequences !== undefined) {
- newSignals = wrapSequences(response.hits.sequences, buildReasonMessageForEqlAlert);
- } else if (response.hits.events !== undefined) {
- newSignals = wrapHits(response.hits.events, buildReasonMessageForEqlAlert);
- } else {
- throw new Error(
- 'eql query response should have either `sequences` or `events` but had neither'
- );
- }
+ let newSignals: Array> | undefined;
+ if (response.hits.sequences !== undefined) {
+ newSignals = wrapSequences(response.hits.sequences, buildReasonMessageForEqlAlert);
+ } else if (response.hits.events !== undefined) {
+ newSignals = wrapHits(response.hits.events, buildReasonMessageForEqlAlert);
+ } else {
+ throw new Error(
+ 'eql query response should have either `sequences` or `events` but had neither'
+ );
+ }
- if (newSignals?.length) {
- const createResult = await bulkCreate(
- newSignals,
- undefined,
- createEnrichEventsFunction({
- services,
- logger: ruleExecutionLogger,
- })
- );
+ if (newSignals?.length) {
+ const createResult = await bulkCreate(
+ newSignals,
+ undefined,
+ createEnrichEventsFunction({
+ services,
+ logger: ruleExecutionLogger,
+ })
+ );
- addToSearchAfterReturn({ current: result, next: createResult });
- }
- if (response.hits.total && response.hits.total.value >= ruleParams.maxSignals) {
- result.warningMessages.push(getMaxSignalsWarning());
+ addToSearchAfterReturn({ current: result, next: createResult });
+ }
+ if (response.hits.total && response.hits.total.value >= ruleParams.maxSignals) {
+ result.warningMessages.push(getMaxSignalsWarning());
+ }
+ return result;
+ } catch (error) {
+ if (
+ typeof error.message === 'string' &&
+ (error.message as string).includes('verification_exception')
+ ) {
+ // We report errors that are more related to user configuration of rules rather than system outages as "user errors"
+ // so SLO dashboards can show less noise around system outages
+ result.userError = true;
+ }
+ result.errors.push(error.message);
+ result.success = false;
+ return result;
}
- return result;
});
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts
index d1adcc535c1cc..c357a7e077bb2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts
@@ -64,6 +64,7 @@ describe('ml_executor', () => {
errors: [],
createdItems: [],
});
+ jobsSummaryMock.mockResolvedValue([]);
});
it('should throw an error if ML plugin was not available', async () => {
@@ -131,4 +132,26 @@ describe('ml_executor', () => {
);
expect(response.warningMessages.length).toEqual(1);
});
+
+ it('should report job missing errors as user errors', async () => {
+ (findMlSignals as jest.Mock).mockRejectedValue({
+ message: 'my_test_job_name missing',
+ });
+
+ const result = await mlExecutor({
+ completeRule: mlCompleteRule,
+ tuple,
+ ml: mlMock,
+ services: alertServices,
+ ruleExecutionLogger,
+ listClient,
+ bulkCreate: jest.fn(),
+ wrapHits: jest.fn(),
+ exceptionFilter: undefined,
+ unprocessedExceptions: [],
+ });
+ expect(result.userError).toEqual(true);
+ expect(result.success).toEqual(false);
+ expect(result.errors).toEqual(['my_test_job_name missing']);
+ });
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts
index 0e937987ec586..641a9dab05cb2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+/* eslint require-atomic-updates: ["error", { "allowProperties": true }] */
+
import type { KibanaRequest } from '@kbn/core/server';
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import type {
@@ -30,6 +32,7 @@ import {
import type { SetupPlugins } from '../../../../plugin';
import { withSecuritySpan } from '../../../../utils/with_security_span';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
+import type { AnomalyResults } from '../../../machine_learning';
export const mlExecutor = async ({
completeRule,
@@ -93,19 +96,29 @@ export const mlExecutor = async ({
result.warning = true;
}
- const anomalyResults = await findMlSignals({
- ml,
- // Using fake KibanaRequest as it is needed to satisfy the ML Services API, but can be empty as it is
- // currently unused by the mlAnomalySearch function.
- request: {} as unknown as KibanaRequest,
- savedObjectsClient: services.savedObjectsClient,
- jobIds: ruleParams.machineLearningJobId,
- anomalyThreshold: ruleParams.anomalyThreshold,
- from: tuple.from.toISOString(),
- to: tuple.to.toISOString(),
- maxSignals: tuple.maxSignals,
- exceptionFilter,
- });
+ let anomalyResults: AnomalyResults;
+ try {
+ anomalyResults = await findMlSignals({
+ ml,
+ // Using fake KibanaRequest as it is needed to satisfy the ML Services API, but can be empty as it is
+ // currently unused by the mlAnomalySearch function.
+ request: {} as unknown as KibanaRequest,
+ savedObjectsClient: services.savedObjectsClient,
+ jobIds: ruleParams.machineLearningJobId,
+ anomalyThreshold: ruleParams.anomalyThreshold,
+ from: tuple.from.toISOString(),
+ to: tuple.to.toISOString(),
+ maxSignals: tuple.maxSignals,
+ exceptionFilter,
+ });
+ } catch (error) {
+ if (typeof error.message === 'string' && (error.message as string).endsWith('missing')) {
+ result.userError = true;
+ }
+ result.errors.push(error.message);
+ result.success = false;
+ return result;
+ }
if (
anomalyResults.hits.total &&
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts
index 6bd0223652497..ad7e11f72217f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts
@@ -65,6 +65,7 @@ export interface SecurityAlertTypeReturnValue {
createdSignalsCount: number;
createdSignals: unknown[];
errors: string[];
+ userError?: boolean;
lastLookbackDate?: Date | null;
searchAfterTimes: string[];
state: TState;
@@ -388,6 +389,7 @@ export interface SearchAfterAndBulkCreateReturnType {
createdSignalsCount: number;
createdSignals: unknown[];
errors: string[];
+ userError?: boolean;
warningMessages: string[];
suppressedAlertsCount?: number;
}
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts
index 2b7912cc611af..faa2697e39039 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts
@@ -6,6 +6,8 @@
*/
import { v4 as uuidv4 } from 'uuid';
+import supertestLib from 'supertest';
+import url from 'url';
import expect from '@kbn/expect';
import {
ALERT_REASON,
@@ -30,7 +32,10 @@ import {
ALERT_GROUP_ID,
} from '@kbn/security-solution-plugin/common/field_maps/field_names';
import { getMaxSignalsWarning as getMaxAlertsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
-import { ENABLE_ASSET_CRITICALITY_SETTING } from '@kbn/security-solution-plugin/common/constants';
+import {
+ DETECTION_ENGINE_RULES_URL,
+ ENABLE_ASSET_CRITICALITY_SETTING,
+} from '@kbn/security-solution-plugin/common/constants';
import {
getEqlRuleForAlertTesting,
getAlerts,
@@ -42,6 +47,8 @@ import {
createRule,
deleteAllRules,
deleteAllAlerts,
+ waitForRuleFailure,
+ routeWithNamespace,
} from '../../../../../../../common/utils/security_solution';
import { FtrProviderContext } from '../../../../../../ftr_provider_context';
import { EsArchivePathBuilder } from '../../../../../../es_archive_path_builder';
@@ -62,6 +69,7 @@ export default ({ getService }: FtrProviderContext) => {
// TODO: add a new service for loading archiver files similar to "getService('es')"
const config = getService('config');
+ const request = supertestLib(url.format(config.get('servers.kibana')));
const isServerless = config.get('serverless');
const dataPathBuilder = new EsArchivePathBuilder(isServerless);
const auditPath = dataPathBuilder.getPath('auditbeat/hosts');
@@ -196,6 +204,42 @@ export default ({ getService }: FtrProviderContext) => {
});
});
+ it('classifies verification_exception errors as user errors', async () => {
+ function getMetricsRequest(reset: boolean = false) {
+ return request
+ .get(`/api/task_manager/metrics${reset ? '' : '?reset=false'}`)
+ .set('kbn-xsrf', 'foo')
+ .expect(200)
+ .then((response) => response.body);
+ }
+
+ await getMetricsRequest(true);
+ const rule: EqlRuleCreateProps = {
+ ...getEqlRuleForAlertTesting(['auditbeat-*']),
+ query: 'file where field.doesnt.exist == true',
+ };
+ const createdRule = await createRule(supertest, log, rule);
+ await waitForRuleFailure({ supertest, log, id: createdRule.id });
+
+ const route = routeWithNamespace(DETECTION_ENGINE_RULES_URL);
+ const response = await supertest
+ .get(route)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '2023-10-31')
+ .query({ id: createdRule.id })
+ .expect(200);
+
+ const ruleResponse = response.body;
+ expect(
+ ruleResponse.execution_summary.last_execution.message.includes('verification_exception')
+ ).eql(true);
+
+ const metricsResponse = await getMetricsRequest();
+ expect(
+ metricsResponse.metrics.task_run.value.by_type['alerting:siem__eqlRule'].user_errors
+ ).eql(1);
+ });
+
it('generates up to max_alerts for non-sequence EQL queries', async () => {
const maxAlerts = 200;
const rule: EqlRuleCreateProps = {
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts
index 157228fe53c10..04454e80707a0 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import supertestLib from 'supertest';
+import url from 'url';
import {
ALERT_REASON,
ALERT_RISK_SCORE,
@@ -27,7 +29,10 @@ import {
} from '@kbn/security-solution-plugin/common/field_maps/field_names';
import { getMaxSignalsWarning as getMaxAlertsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
import { expect } from 'expect';
-import { ENABLE_ASSET_CRITICALITY_SETTING } from '@kbn/security-solution-plugin/common/constants';
+import {
+ DETECTION_ENGINE_RULES_URL,
+ ENABLE_ASSET_CRITICALITY_SETTING,
+} from '@kbn/security-solution-plugin/common/constants';
import {
createListsIndex,
deleteAllExceptions,
@@ -46,6 +51,8 @@ import {
createRule,
deleteAllRules,
deleteAllAlerts,
+ waitForRuleFailure,
+ routeWithNamespace,
} from '../../../../../../../common/utils/security_solution';
import { FtrProviderContext } from '../../../../../../ftr_provider_context';
import { EsArchivePathBuilder } from '../../../../../../es_archive_path_builder';
@@ -58,6 +65,7 @@ export default ({ getService }: FtrProviderContext) => {
const kibanaServer = getService('kibanaServer');
// TODO: add a new service for loading archiver files similar to "getService('es')"
const config = getService('config');
+ const request = supertestLib(url.format(config.get('servers.kibana')));
const isServerless = config.get('serverless');
const dataPathBuilder = new EsArchivePathBuilder(isServerless);
const auditPath = dataPathBuilder.getPath('auditbeat/hosts');
@@ -172,6 +180,42 @@ export default ({ getService }: FtrProviderContext) => {
);
});
+ it('classifies ml job missing errors as user errors', async () => {
+ function getMetricsRequest(reset: boolean = false) {
+ return request
+ .get(`/api/task_manager/metrics${reset ? '' : '?reset=false'}`)
+ .set('kbn-xsrf', 'foo')
+ .expect(200)
+ .then((response) => response.body);
+ }
+
+ await getMetricsRequest(true);
+ const badRule: MachineLearningRuleCreateProps = {
+ ...rule,
+ machine_learning_job_id: 'doesNotExist',
+ };
+ const createdRule = await createRule(supertest, log, badRule);
+ await waitForRuleFailure({ supertest, log, id: createdRule.id });
+
+ const route = routeWithNamespace(DETECTION_ENGINE_RULES_URL);
+ const response = await supertest
+ .get(route)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '2023-10-31')
+ .query({ id: createdRule.id })
+ .expect(200);
+
+ const ruleResponse = response.body;
+ expect(ruleResponse.execution_summary.last_execution.message.includes('missing')).toEqual(
+ true
+ );
+
+ const metricsResponse = await getMetricsRequest();
+ expect(
+ metricsResponse.metrics.task_run.value.by_type['alerting:siem__mlRule'].user_errors
+ ).toEqual(1);
+ });
+
it('@skipInQA generates max alerts warning when circuit breaker is exceeded', async () => {
const { logs } = await previewRule({
supertest,
From db10b19340dfe67150827671e32190be68a79883 Mon Sep 17 00:00:00 2001
From: Milton Hultgren
Date: Wed, 10 Apr 2024 16:39:07 +0200
Subject: [PATCH 15/55] [Obs AI Assistant] Support user instructions via
Knowledge base or API request (#180263)
Part of #179736
### Summary
This PR adds the initial support for user instructions by:
1. Reading entries from the Knowledge base marked with label `category`
having the value `instruction`
2. Accepting instructions via the `/chat/complete` endpoint
3. Merging these two sources of instructions, giving priority to those
passed via the request to `/chat/complete`
### How to test
Using either the UI or the API, make a request to `/chat/complete`
asking about any topic.
Then create a basic instruction inside the Knowledge base that should
alter the LLMs interpretation of the question:
```
POST kbn:/internal/observability_ai_assistant/kb/entries/save
{
"id": "my_instruction",
"text": "While your main role is to help with Observability tasks, feel free to share your knowledge about Lord of the rings with the user.",
"labels": {
"category": "instruction"
}
}
```
Then make another request to `/chat/complete` and see how the response
changed.
To validate that instructions sent via the request take priority, make
another request that specifies the same `doc_id` as the one entered into
the knowledge base and repeat the request.
---
.../common/types.ts | 5 ++
.../common/utils/extend_system_message.tsx | 22 +++++++
.../common/utils/with_token_budget.ts | 48 +++++++++++++++
.../server/routes/chat/route.ts | 11 ++++
.../server/service/client/index.test.ts | 34 ++++++-----
.../server/service/client/index.ts | 58 ++++++++++++++-----
.../service/knowledge_base_service/index.ts | 41 ++++++++++++-
.../server/service/util/get_category_query.ts | 4 +-
.../public/components/chat/chat_body.tsx | 5 +-
9 files changed, 194 insertions(+), 34 deletions(-)
create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/extend_system_message.tsx
create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/with_token_budget.ts
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts
index e30f425d5dd9a..16b088f07f497 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts
@@ -88,6 +88,11 @@ export interface KnowledgeBaseEntry {
role: KnowledgeBaseEntryRole;
}
+export interface UserInstruction {
+ doc_id: string;
+ text: string;
+}
+
export interface ObservabilityAIAssistantScreenContextRequest {
screenDescription?: string;
data?: Array<{
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/extend_system_message.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/extend_system_message.tsx
new file mode 100644
index 0000000000000..77a7d99c763ee
--- /dev/null
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/extend_system_message.tsx
@@ -0,0 +1,22 @@
+/*
+ * 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 { Message } from '../types';
+
+export function extendSystemMessage(messages: Message[], extensions: string[]) {
+ const [systemMessage, ...rest] = messages;
+
+ const extendedSystemMessage: Message = {
+ ...systemMessage,
+ message: {
+ ...systemMessage.message,
+ content: `${systemMessage.message.content}\n\n${extensions.join('\n\n').trim()}`,
+ },
+ };
+
+ return [extendedSystemMessage].concat(rest);
+}
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/with_token_budget.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/with_token_budget.ts
new file mode 100644
index 0000000000000..2c0eb41edfb9a
--- /dev/null
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/with_token_budget.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { encode } from 'gpt-tokenizer';
+
+export function withTokenBudget(
+ items: T[],
+ budget: number,
+ options?: {
+ contentAccessor?: (item: T) => string;
+ maximizeBudget?: boolean;
+ }
+) {
+ const contentAccessor = options?.contentAccessor ?? asStringAccessor;
+ const maximizeBudget = options?.maximizeBudget ?? false;
+
+ const itemsWithinBudget: T[] = [];
+ let usedBudget = 0;
+
+ for (const item of items) {
+ const content = contentAccessor(item);
+ const tokenCount = encode(content).length;
+ const fitsInBudget = usedBudget + tokenCount <= budget;
+
+ if (fitsInBudget) {
+ itemsWithinBudget.push(item);
+ usedBudget += tokenCount;
+ } else if (maximizeBudget) {
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ return itemsWithinBudget;
+}
+
+function asStringAccessor(item: unknown) {
+ if (typeof item === 'string') {
+ return item;
+ }
+
+ return JSON.stringify(item);
+}
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts
index 7d375a6ae9d2c..319ea153eea84 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts
@@ -96,6 +96,15 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({
conversationId: t.string,
title: t.string,
responseLanguage: t.string,
+ instructions: t.array(
+ t.union([
+ t.string,
+ t.type({
+ doc_id: t.string,
+ text: t.string,
+ }),
+ ])
+ ),
}),
]),
}),
@@ -120,6 +129,7 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({
persist,
screenContexts,
responseLanguage,
+ instructions,
},
} = params;
@@ -145,6 +155,7 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({
signal: controller.signal,
functionClient,
responseLanguage,
+ instructions,
});
return observableIntoStream(response$.pipe(flushBuffer(!!cloudStart?.isCloudEnabled)));
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.test.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.test.ts
index edc69bd3c2811..f0a1a29b9b86b 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.test.ts
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.test.ts
@@ -103,6 +103,7 @@ describe('Observability AI Assistant client', () => {
const knowledgeBaseServiceMock: DeeplyMockedKeys = {
recall: jest.fn(),
+ getInstructions: jest.fn(),
} as any;
const loggerMock: DeeplyMockedKeys = {
@@ -153,6 +154,8 @@ describe('Observability AI Assistant client', () => {
fields: [],
} as any);
+ knowledgeBaseServiceMock.getInstructions.mockResolvedValue([]);
+
return new ObservabilityAIAssistantClient({
actionsClient: actionsClientMock,
esClient: {
@@ -206,14 +209,6 @@ describe('Observability AI Assistant client', () => {
beforeEach(async () => {
client = createClient();
actionsClientMock.execute
- .mockImplementationOnce(async () => {
- llmSimulator = createLlmSimulator();
- return {
- actionId: '',
- status: 'ok',
- data: llmSimulator.stream,
- };
- })
.mockImplementationOnce(() => {
return new Promise((resolve, reject) => {
titleLlmPromiseResolve = (title: string) => {
@@ -230,6 +225,14 @@ describe('Observability AI Assistant client', () => {
reject();
};
});
+ })
+ .mockImplementationOnce(async () => {
+ llmSimulator = createLlmSimulator();
+ return {
+ actionId: '',
+ status: 'ok',
+ data: llmSimulator.stream,
+ };
});
stream = observableIntoStream(
@@ -362,6 +365,7 @@ describe('Observability AI Assistant client', () => {
await finished(stream);
});
+
it('adds the completed message to the stream', () => {
expect(JSON.parse(dataHandler.mock.calls[1])).toEqual({
id: expect.any(String),
@@ -431,7 +435,7 @@ describe('Observability AI Assistant client', () => {
'@timestamp': expect.any(String),
message: {
content:
- 'You MUST respond in the users preferred language which is: English. This is a system message',
+ 'This is a system message\n\nYou MUST respond in the users preferred language which is: English.',
role: MessageRole.System,
},
},
@@ -577,7 +581,7 @@ describe('Observability AI Assistant client', () => {
'@timestamp': expect.any(String),
message: {
content:
- 'You MUST respond in the users preferred language which is: English. This is a system message',
+ 'This is a system message\n\nYou MUST respond in the users preferred language which is: English.',
role: MessageRole.System,
},
},
@@ -798,7 +802,7 @@ describe('Observability AI Assistant client', () => {
message: {
role: MessageRole.System,
content:
- 'You MUST respond in the users preferred language which is: English. This is a system message',
+ 'This is a system message\n\nYou MUST respond in the users preferred language which is: English.',
},
},
{
@@ -931,7 +935,7 @@ describe('Observability AI Assistant client', () => {
'@timestamp': expect.any(String),
message: {
content:
- 'You MUST respond in the users preferred language which is: English. This is a system message',
+ 'This is a system message\n\nYou MUST respond in the users preferred language which is: English.',
role: MessageRole.System,
},
},
@@ -1469,9 +1473,10 @@ describe('Observability AI Assistant client', () => {
persist: false,
})
.subscribe(() => {}); // To trigger call to chat
+ await nextTick();
expect(chatSpy.mock.calls[0][1].messages[0].message.content).toEqual(
- 'You MUST respond in the users preferred language which is: English. This is a system message'
+ 'This is a system message\n\nYou MUST respond in the users preferred language which is: English.'
);
});
@@ -1498,9 +1503,10 @@ describe('Observability AI Assistant client', () => {
responseLanguage: 'Orcish',
})
.subscribe(() => {}); // To trigger call to chat
+ await nextTick();
expect(chatSpy.mock.calls[0][1].messages[0].message.content).toEqual(
- 'You MUST respond in the users preferred language which is: Orcish. This is a system message'
+ 'This is a system message\n\nYou MUST respond in the users preferred language which is: Orcish.'
);
});
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts
index 679b1fca1f249..2994c40379af0 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts
@@ -25,6 +25,8 @@ import {
} from 'rxjs';
import { Readable } from 'stream';
import { v4 } from 'uuid';
+import { withTokenBudget } from '../../../common/utils/with_token_budget';
+import { extendSystemMessage } from '../../../common/utils/extend_system_message';
import { ObservabilityAIAssistantConnectorType } from '../../../common/connectors';
import {
ChatCompletionChunkEvent,
@@ -43,6 +45,7 @@ import {
} from '../../../common/functions/types';
import {
MessageRole,
+ UserInstruction,
type Conversation,
type ConversationCreateRequest,
type ConversationUpdateRequest,
@@ -149,6 +152,7 @@ export class ObservabilityAIAssistantClient {
responseLanguage?: string;
conversationId?: string;
title?: string;
+ instructions?: Array;
}): Observable> => {
return new Observable>(
(subscriber) => {
@@ -157,6 +161,7 @@ export class ObservabilityAIAssistantClient {
const conversationId = params.conversationId || '';
const title = params.title || '';
const responseLanguage = params.responseLanguage || 'English';
+ const requestInstructions = params.instructions || [];
const tokenCountResult = {
prompt: 0,
@@ -511,12 +516,21 @@ export class ObservabilityAIAssistantClient {
subscriber.complete();
};
- next(this.addResponseLanguage(messages, responseLanguage)).catch((error) => {
- if (!signal.aborted) {
- this.dependencies.logger.error(error);
- }
- subscriber.error(error);
- });
+ this.resolveInstructions(requestInstructions)
+ .then((instructions) => {
+ return next(
+ extendSystemMessage(messages, [
+ `You MUST respond in the users preferred language which is: ${responseLanguage}.`,
+ instructions,
+ ])
+ );
+ })
+ .catch((error) => {
+ if (!signal.aborted) {
+ this.dependencies.logger.error(error);
+ }
+ subscriber.error(error);
+ });
const titlePromise =
!conversationId && !title && persist
@@ -773,6 +787,7 @@ export class ObservabilityAIAssistantClient {
},
},
],
+ functionCall: 'title_conversation',
connectorId,
signal,
});
@@ -911,17 +926,28 @@ export class ObservabilityAIAssistantClient {
return this.dependencies.knowledgeBaseService.deleteEntry({ id });
};
- private addResponseLanguage = (messages: Message[], responseLanguage: string): Message[] => {
- const [systemMessage, ...rest] = messages;
+ private resolveInstructions = async (requestInstructions: Array) => {
+ const knowledgeBaseInstructions = await this.dependencies.knowledgeBaseService.getInstructions(
+ this.dependencies.user,
+ this.dependencies.namespace
+ );
- const extendedSystemMessage: Message = {
- ...systemMessage,
- message: {
- ...systemMessage.message,
- content: `You MUST respond in the users preferred language which is: ${responseLanguage}. ${systemMessage.message.content}`,
- },
- };
+ if (requestInstructions.length + knowledgeBaseInstructions.length === 0) {
+ return '';
+ }
+
+ const priorityInstructions = requestInstructions.map((instruction) =>
+ typeof instruction === 'string' ? { doc_id: v4(), text: instruction } : instruction
+ );
+ const overrideIds = priorityInstructions.map((instruction) => instruction.doc_id);
+ const instructions = priorityInstructions.concat(
+ knowledgeBaseInstructions.filter((instruction) => !overrideIds.includes(instruction.doc_id))
+ );
+
+ const instructionsWithinBudget = withTokenBudget(instructions, 1000);
+
+ const instructionsPrompt = `What follows is a set of instructions provided by the user, please abide by them as long as they don't conflict with anything you've been told so far:\n`;
- return [extendedSystemMessage].concat(rest);
+ return `${instructionsPrompt}${instructionsWithinBudget.join('\n\n')}`;
};
}
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts
index 56e0c34024b47..126b977facec2 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts
@@ -14,7 +14,7 @@ import pRetry from 'p-retry';
import { map, orderBy } from 'lodash';
import { encode } from 'gpt-tokenizer';
import { INDEX_QUEUED_DOCUMENTS_TASK_ID, INDEX_QUEUED_DOCUMENTS_TASK_TYPE } from '..';
-import { KnowledgeBaseEntry, KnowledgeBaseEntryRole } from '../../../common/types';
+import { KnowledgeBaseEntry, KnowledgeBaseEntryRole, UserInstruction } from '../../../common/types';
import type { ObservabilityAIAssistantResourceNames } from '../types';
import { getAccessQuery } from '../util/get_access_query';
import { getCategoryQuery } from '../util/get_category_query';
@@ -492,6 +492,45 @@ export class KnowledgeBaseService {
};
};
+ getInstructions = async (
+ user: { name: string },
+ namespace: string
+ ): Promise => {
+ try {
+ const response = await this.dependencies.esClient.search({
+ index: this.dependencies.resources.aliases.kb,
+ query: {
+ bool: {
+ must: [
+ {
+ term: {
+ 'labels.category.keyword': {
+ value: 'instruction',
+ },
+ },
+ },
+ ],
+ filter: getAccessQuery({
+ user,
+ namespace,
+ }),
+ },
+ },
+ size: 500,
+ _source: ['doc_id', 'text'],
+ });
+
+ return response.hits.hits.map((hit) => ({
+ doc_id: hit._source?.doc_id ?? '',
+ text: hit._source?.text ?? '',
+ }));
+ } catch (error) {
+ this.dependencies.logger.error('Failed to load instructions from knowledge base');
+ this.dependencies.logger.error(error);
+ return [];
+ }
+ };
+
getEntries = async ({
query,
sortBy,
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_category_query.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_category_query.ts
index 6fb233a176afe..d6202fb59861a 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_category_query.ts
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_category_query.ts
@@ -10,7 +10,7 @@ export function getCategoryQuery({ categories }: { categories?: string[] }) {
bool: {
must_not: {
exists: {
- field: 'labels.category',
+ field: 'labels.category.keyword',
},
},
},
@@ -27,7 +27,7 @@ export function getCategoryQuery({ categories }: { categories?: string[] }) {
noCategoryFilter,
{
terms: {
- 'labels.category': categories,
+ 'labels.category.keyword': categories,
},
},
],
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.tsx
index 4a5d7d38c3e63..1bf95cffe8b64 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.tsx
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.tsx
@@ -220,7 +220,10 @@ export function ChatBody({
});
const handleCopyConversation = () => {
- const content = JSON.stringify({ title: initialTitle, messages });
+ const content = JSON.stringify({
+ title: initialTitle,
+ messages: conversation.value?.messages ?? messages,
+ });
navigator.clipboard?.writeText(content || '');
};
From 099c073c8c7abd28af43d2fdf9eea043bd238b19 Mon Sep 17 00:00:00 2001
From: Julia Rechkunova
Date: Wed, 10 Apr 2024 16:51:15 +0200
Subject: [PATCH 16/55] [Discover][ES|QL] No legacy table for ES|QL mode
(#180370)
- Closes https://github.com/elastic/kibana/issues/180286
## Summary
Even if `doc_table:legacy` is enabled this PR makes sure that we render
the new grid for ES|QL mode.
### 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
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
packages/kbn-discover-utils/index.ts | 1 +
.../kbn-discover-utils/src/utils/index.ts | 1 +
.../src/utils/is_legacy_table_enabled.ts | 24 +++++
packages/kbn-discover-utils/tsconfig.json | 3 +-
.../components/layout/discover_documents.tsx | 10 +-
.../components/top_nav/on_save_search.tsx | 8 +-
.../view_mode_toggle/view_mode_toggle.tsx | 7 +-
.../embeddable/saved_search_embeddable.tsx | 10 +-
.../components/doc_viewer_source/source.tsx | 7 +-
.../unified_doc_viewer/public/plugin.tsx | 10 +-
.../apps/discover/classic/_esql_grid.ts | 95 +++++++++++++++++++
.../functional/apps/discover/classic/index.ts | 1 +
.../discover/group3/_time_field_column.ts | 22 ++---
13 files changed, 168 insertions(+), 31 deletions(-)
create mode 100644 packages/kbn-discover-utils/src/utils/is_legacy_table_enabled.ts
create mode 100644 test/functional/apps/discover/classic/_esql_grid.ts
diff --git a/packages/kbn-discover-utils/index.ts b/packages/kbn-discover-utils/index.ts
index 5eb2650482611..a409962230ce6 100644
--- a/packages/kbn-discover-utils/index.ts
+++ b/packages/kbn-discover-utils/index.ts
@@ -37,5 +37,6 @@ export {
getIgnoredReason,
getShouldShowFieldHandler,
isNestedFieldParent,
+ isLegacyTableEnabled,
usePager,
} from './src';
diff --git a/packages/kbn-discover-utils/src/utils/index.ts b/packages/kbn-discover-utils/src/utils/index.ts
index 4828fcf82a447..8415fc7df0710 100644
--- a/packages/kbn-discover-utils/src/utils/index.ts
+++ b/packages/kbn-discover-utils/src/utils/index.ts
@@ -13,3 +13,4 @@ export * from './get_doc_id';
export * from './get_ignored_reason';
export * from './get_should_show_field_handler';
export * from './nested_fields';
+export { isLegacyTableEnabled } from './is_legacy_table_enabled';
diff --git a/packages/kbn-discover-utils/src/utils/is_legacy_table_enabled.ts b/packages/kbn-discover-utils/src/utils/is_legacy_table_enabled.ts
new file mode 100644
index 0000000000000..7e575cf80dbfb
--- /dev/null
+++ b/packages/kbn-discover-utils/src/utils/is_legacy_table_enabled.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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
+import { DOC_TABLE_LEGACY } from '../constants';
+
+export function isLegacyTableEnabled({
+ uiSettings,
+ isTextBasedQueryMode,
+}: {
+ uiSettings: IUiSettingsClient;
+ isTextBasedQueryMode: boolean;
+}): boolean {
+ if (isTextBasedQueryMode) {
+ return false; // only show the new data grid
+ }
+
+ return uiSettings.get(DOC_TABLE_LEGACY);
+}
diff --git a/packages/kbn-discover-utils/tsconfig.json b/packages/kbn-discover-utils/tsconfig.json
index 0051ad5b2a00a..64453f8245afe 100644
--- a/packages/kbn-discover-utils/tsconfig.json
+++ b/packages/kbn-discover-utils/tsconfig.json
@@ -21,6 +21,7 @@
"@kbn/es-query",
"@kbn/field-formats-plugin",
"@kbn/field-types",
- "@kbn/i18n"
+ "@kbn/i18n",
+ "@kbn/core-ui-settings-browser"
]
}
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
index 4c0f01d04b6eb..6d123ab057deb 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
@@ -29,8 +29,8 @@ import {
} from '@kbn/unified-data-table';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
- DOC_TABLE_LEGACY,
HIDE_ANNOUNCEMENTS,
+ isLegacyTableEnabled,
MAX_DOC_FIELDS_DISPLAYED,
ROW_HEIGHT_OPTION,
SEARCH_FIELDS_FROM_SOURCE,
@@ -140,15 +140,19 @@ function DiscoverDocumentsComponent({
const expandedDoc = useInternalStateSelector((state) => state.expandedDoc);
+ const isTextBasedQuery = useMemo(() => getRawRecordType(query) === RecordRawType.PLAIN, [query]);
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
const hideAnnouncements = useMemo(() => uiSettings.get(HIDE_ANNOUNCEMENTS), [uiSettings]);
- const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]);
+ const isLegacy = useMemo(
+ () => isLegacyTableEnabled({ uiSettings, isTextBasedQueryMode: isTextBasedQuery }),
+ [uiSettings, isTextBasedQuery]
+ );
const documentState = useDataState(documents$);
const isDataLoading =
documentState.fetchStatus === FetchStatus.LOADING ||
documentState.fetchStatus === FetchStatus.PARTIAL;
- const isTextBasedQuery = useMemo(() => getRawRecordType(query) === RecordRawType.PLAIN, [query]);
+
// This is needed to prevent EuiDataGrid pushing onSort because the data view has been switched.
// It's just necessary for non-text-based query lang requests since they don't have a partial result state, that's
// considered as loading state in the Component.
diff --git a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
index f22d07b4d4d89..6414d7b5685fa 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
@@ -10,9 +10,10 @@ import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiSwitch } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
+import { isOfAggregateQueryType } from '@kbn/es-query';
import { SavedObjectSaveModal, showSaveModal, OnSaveProps } from '@kbn/saved-objects-plugin/public';
import { SavedSearch, SaveSavedSearchOptions } from '@kbn/saved-search-plugin/public';
-import { DOC_TABLE_LEGACY } from '@kbn/discover-utils';
+import { isLegacyTableEnabled } from '@kbn/discover-utils';
import { DiscoverServices } from '../../../../build_services';
import { DiscoverStateContainer } from '../../services/discover_state';
import { getAllowedSampleSize } from '../../../../utils/get_allowed_sample_size';
@@ -123,7 +124,10 @@ export async function onSaveSearch({
savedSearch.title = newTitle;
savedSearch.description = newDescription;
savedSearch.timeRestore = newTimeRestore;
- savedSearch.rowsPerPage = uiSettings.get(DOC_TABLE_LEGACY)
+ savedSearch.rowsPerPage = isLegacyTableEnabled({
+ uiSettings,
+ isTextBasedQueryMode: isOfAggregateQueryType(savedSearch.searchSource.getField('query')),
+ })
? currentRowsPerPage
: state.appState.getState().rowsPerPage;
diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx
index 147486ac6dc6e..98b9ac12ef056 100644
--- a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx
+++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx
@@ -10,7 +10,7 @@ import React, { useMemo, ReactElement } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs, useEuiTheme } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
-import { DOC_TABLE_LEGACY, SHOW_FIELD_STATISTICS } from '@kbn/discover-utils';
+import { isLegacyTableEnabled, SHOW_FIELD_STATISTICS } from '@kbn/discover-utils';
import { VIEW_MODE } from '../../../common/constants';
import { useDiscoverServices } from '../../hooks/use_discover_services';
import { DiscoverStateContainer } from '../../application/main/services/discover_state';
@@ -31,7 +31,10 @@ export const DocumentViewModeToggle = ({
}) => {
const { euiTheme } = useEuiTheme();
const { uiSettings } = useDiscoverServices();
- const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]);
+ const isLegacy = useMemo(
+ () => isLegacyTableEnabled({ uiSettings, isTextBasedQueryMode: isTextBasedQuery }),
+ [uiSettings, isTextBasedQuery]
+ );
const includesNormalTabsStyle = viewMode === VIEW_MODE.AGGREGATED_LEVEL || isLegacy;
const containerPadding = includesNormalTabsStyle ? euiTheme.size.s : 0;
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
index 76fda6fee1a98..e8b0163923162 100644
--- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
@@ -49,11 +49,11 @@ import type { SearchResponseWarning } from '@kbn/search-response-warnings';
import type { EsHitRecord } from '@kbn/discover-utils/types';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
- DOC_TABLE_LEGACY,
SEARCH_FIELDS_FROM_SOURCE,
SHOW_FIELD_STATISTICS,
SORT_DEFAULT_ORDER_SETTING,
buildDataTableRecord,
+ isLegacyTableEnabled,
} from '@kbn/discover-utils';
import { columnActions, getTextBasedColumnsMeta } from '@kbn/unified-data-table';
import { VIEW_MODE, getDefaultRowsPerPage } from '../../common/constants';
@@ -629,9 +629,10 @@ export class SavedSearchEmbeddable
return;
}
+ const isTextBasedQueryMode = this.isTextBasedSearch(savedSearch);
const viewMode = getValidViewMode({
viewMode: savedSearch.viewMode,
- isTextBasedQueryMode: this.isTextBasedSearch(savedSearch),
+ isTextBasedQueryMode,
});
if (
@@ -669,7 +670,10 @@ export class SavedSearchEmbeddable
return;
}
- const useLegacyTable = this.services.uiSettings.get(DOC_TABLE_LEGACY);
+ const useLegacyTable = isLegacyTableEnabled({
+ uiSettings: this.services.uiSettings,
+ isTextBasedQueryMode,
+ });
const query = savedSearch.searchSource.getField('query');
const props = {
savedSearch,
diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.tsx
index 189cbd2b5be4a..26ac6cb01b71e 100644
--- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.tsx
+++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_source/source.tsx
@@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import { ElasticRequestState } from '@kbn/unified-doc-viewer';
-import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '@kbn/discover-utils';
+import { isLegacyTableEnabled, SEARCH_FIELDS_FROM_SOURCE } from '@kbn/discover-utils';
import { getUnifiedDocViewerServices } from '../../plugin';
import { useEsDocSearch } from '../../hooks';
import { getHeight } from './get_height';
@@ -54,7 +54,10 @@ export const DocViewerSource = ({
const [jsonValue, setJsonValue] = useState('');
const { uiSettings } = getUnifiedDocViewerServices();
const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
- const useDocExplorer = !uiSettings.get(DOC_TABLE_LEGACY);
+ const useDocExplorer = isLegacyTableEnabled({
+ uiSettings,
+ isTextBasedQueryMode: Array.isArray(textBasedHits),
+ });
const [requestState, hit] = useEsDocSearch({
id,
index,
diff --git a/src/plugins/unified_doc_viewer/public/plugin.tsx b/src/plugins/unified_doc_viewer/public/plugin.tsx
index 4b3793872b454..6dc4527441822 100644
--- a/src/plugins/unified_doc_viewer/public/plugin.tsx
+++ b/src/plugins/unified_doc_viewer/public/plugin.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import type { CoreSetup, Plugin } from '@kbn/core/public';
-import { DOC_TABLE_LEGACY } from '@kbn/discover-utils';
+import { isLegacyTableEnabled } from '@kbn/discover-utils';
import { i18n } from '@kbn/i18n';
import { DocViewsRegistry } from '@kbn/unified-doc-viewer';
import { EuiDelayRender, EuiSkeletonText } from '@elastic/eui';
@@ -51,8 +51,14 @@ export class UnifiedDocViewerPublicPlugin
}),
order: 10,
component: (props) => {
+ const { textBasedHits } = props;
const { uiSettings } = getUnifiedDocViewerServices();
- const DocView = uiSettings.get(DOC_TABLE_LEGACY) ? DocViewerLegacyTable : DocViewerTable;
+ const DocView = isLegacyTableEnabled({
+ uiSettings,
+ isTextBasedQueryMode: Array.isArray(textBasedHits),
+ })
+ ? DocViewerLegacyTable
+ : DocViewerTable;
return (
{
+ await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']);
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
+ await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
+ await kibanaServer.uiSettings.replace(defaultSettings);
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.timePicker.setDefaultAbsoluteRange();
+ });
+
+ after(async () => {
+ await kibanaServer.savedObjects.cleanStandardList();
+ await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
+ await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
+ await kibanaServer.uiSettings.replace({});
+ });
+
+ it('should render esql view correctly', async function () {
+ const savedSearchESQL = 'testESQLWithLegacySetting';
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ await testSubjects.existOrFail('docTableHeader');
+ await testSubjects.missingOrFail('euiDataGridBody');
+
+ await PageObjects.discover.selectTextBaseLang();
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ await testSubjects.missingOrFail('docTableHeader');
+ await testSubjects.existOrFail('euiDataGridBody');
+
+ await dataGrid.clickRowToggle({ rowIndex: 0 });
+
+ await testSubjects.existOrFail('docTableDetailsFlyout');
+
+ await PageObjects.discover.saveSearch(savedSearchESQL);
+
+ await PageObjects.common.navigateToApp('dashboard');
+ await PageObjects.dashboard.clickNewDashboard();
+ await PageObjects.timePicker.setDefaultAbsoluteRange();
+ await dashboardAddPanel.clickOpenAddPanel();
+ await dashboardAddPanel.addSavedSearch(savedSearchESQL);
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ await testSubjects.missingOrFail('docTableHeader');
+ await testSubjects.existOrFail('euiDataGridBody');
+
+ await dataGrid.clickRowToggle({ rowIndex: 0 });
+
+ await testSubjects.existOrFail('docTableDetailsFlyout');
+
+ await dashboardPanelActions.removePanelByTitle(savedSearchESQL);
+
+ await dashboardAddPanel.addSavedSearch('A Saved Search');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await testSubjects.existOrFail('docTableHeader');
+ await testSubjects.missingOrFail('euiDataGridBody');
+ });
+ });
+}
diff --git a/test/functional/apps/discover/classic/index.ts b/test/functional/apps/discover/classic/index.ts
index 845b5e2ee5ece..e6dc2f17c7699 100644
--- a/test/functional/apps/discover/classic/index.ts
+++ b/test/functional/apps/discover/classic/index.ts
@@ -27,5 +27,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_field_data_with_fields_api'));
loadTestFile(require.resolve('./_classic_table_doc_navigation'));
loadTestFile(require.resolve('./_hide_announcements'));
+ loadTestFile(require.resolve('./_esql_grid'));
});
}
diff --git a/test/functional/apps/discover/group3/_time_field_column.ts b/test/functional/apps/discover/group3/_time_field_column.ts
index 3a916f99f8fbe..cbcb37a1294e7 100644
--- a/test/functional/apps/discover/group3/_time_field_column.ts
+++ b/test/functional/apps/discover/group3/_time_field_column.ts
@@ -350,7 +350,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
`${SEARCH_NO_COLUMNS}${savedSearchSuffix}ESQL`
);
await PageObjects.discover.waitUntilSearchingHasFinished();
- expect(await docTable.getHeaderFields()).to.eql(
+ expect(await dataGrid.getHeaderFields()).to.eql(
hideTimeFieldColumnSetting ? ['Document'] : ['@timestamp', 'Document']
);
@@ -358,9 +358,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
`${SEARCH_NO_COLUMNS}${savedSearchSuffix}ESQLdrop`
);
await PageObjects.discover.waitUntilSearchingHasFinished();
- expect(await docTable.getHeaderFields()).to.eql(
- hideTimeFieldColumnSetting ? ['Document'] : ['@timestamp', 'Document']
- );
+ expect(await dataGrid.getHeaderFields()).to.eql(['Document']);
// only @timestamp is selected
await PageObjects.discover.loadSavedSearch(
@@ -381,8 +379,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
`${SEARCH_WITH_ONLY_TIMESTAMP}${savedSearchSuffix}ESQL`
);
await PageObjects.discover.waitUntilSearchingHasFinished();
- expect(await docTable.getHeaderFields()).to.eql(
- hideTimeFieldColumnSetting ? ['@timestamp'] : ['@timestamp', '@timestamp']
+ expect(await dataGrid.getHeaderFields()).to.eql(
+ hideTimeFieldColumnSetting ? ['@timestamp'] : ['@timestamp', 'Document']
);
});
@@ -408,11 +406,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
`${SEARCH_WITH_SELECTED_COLUMNS}${savedSearchSuffix}ESQL`
);
await PageObjects.discover.waitUntilSearchingHasFinished();
- expect(await docTable.getHeaderFields()).to.eql(
- hideTimeFieldColumnSetting
- ? ['bytes', 'extension']
- : ['@timestamp', 'bytes', 'extension']
- );
+ expect(await dataGrid.getHeaderFields()).to.eql(['bytes', 'extension']);
// with selected columns and @timestamp
await PageObjects.discover.loadSavedSearch(
@@ -435,11 +429,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
`${SEARCH_WITH_SELECTED_COLUMNS_AND_TIMESTAMP}${savedSearchSuffix}ESQL`
);
await PageObjects.discover.waitUntilSearchingHasFinished();
- expect(await docTable.getHeaderFields()).to.eql(
- hideTimeFieldColumnSetting
- ? ['bytes', 'extension', '@timestamp']
- : ['@timestamp', 'bytes', 'extension', '@timestamp']
- );
+ expect(await dataGrid.getHeaderFields()).to.eql(['bytes', 'extension', '@timestamp']);
});
});
});
From e537deb9b5a93941f23107fe9db2eab2a2aeab08 Mon Sep 17 00:00:00 2001
From: Christos Nasikas
Date: Wed, 10 Apr 2024 18:07:14 +0300
Subject: [PATCH 17/55] [Actions] Authorize system action when create or
editing rules (#180437)
## Summary
In this PR we add the ability for system actions to provide required
kibana feature privileges for which the user should be authorized when
creating or updating a rule. For example, if a user with no permissions
to Cases should not be able to create a rule with a case system action.
### Checklist
Delete any items that are not applicable to this PR.
- [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
### For maintainers
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---
.../methods/bulk_edit/bulk_edit_rules.test.ts | 108 ++++++++++++++++++
.../rule/methods/bulk_edit/bulk_edit_rules.ts | 6 +-
.../rule/methods/create/create_rule.test.ts | 30 +++++
.../rule/methods/create/create_rule.ts | 10 +-
.../alerting_authorization.test.ts | 43 +++++++
.../authorization/alerting_authorization.ts | 7 +-
...et_system_action_kibana_privileges.test.ts | 102 +++++++++++++++++
.../get_system_action_kibana_privileges.ts | 29 +++++
.../server/connector_adapters/types.ts | 1 +
...validate_authorize_system_actions.test.ts} | 101 ++++++++++++++--
...s => validate_authorize_system_actions.ts} | 17 ++-
.../server/rules_client/methods/update.ts | 6 +-
.../server/rules_client/tests/update.test.ts | 37 ++++++
.../plugins/alerts/server/action_types.ts | 8 +-
.../alerts/server/connector_adapters.ts | 13 +++
.../group1/tests/alerting/create.ts | 76 +++++++++++-
.../group2/tests/alerting/update.ts | 92 ++++++++++++++-
.../group3/tests/alerting/bulk_edit.ts | 99 +++++++++++++++-
18 files changed, 757 insertions(+), 28 deletions(-)
create mode 100644 x-pack/plugins/alerting/server/connector_adapters/get_system_action_kibana_privileges.test.ts
create mode 100644 x-pack/plugins/alerting/server/connector_adapters/get_system_action_kibana_privileges.ts
rename x-pack/plugins/alerting/server/lib/{validate_system_actions.test.ts => validate_authorize_system_actions.test.ts} (65%)
rename x-pack/plugins/alerting/server/lib/{validate_system_actions.ts => validate_authorize_system_actions.ts} (77%)
diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts
index bc8ca1606e43e..d916a81b5fc81 100644
--- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts
+++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts
@@ -1581,6 +1581,114 @@ describe('bulkEdit()', () => {
}
`);
});
+
+ test('should throw an error if the user does not have privileges to execute the action', async () => {
+ const defaultAction = {
+ frequency: {
+ notifyWhen: 'onActiveAlert' as const,
+ summary: false,
+ throttle: null,
+ },
+ group: 'default',
+ id: '1',
+ params: {},
+ };
+
+ const systemAction = {
+ id: 'system_action-id',
+ params: {},
+ };
+
+ unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
+ saved_objects: [
+ {
+ ...existingRule,
+ attributes: {
+ ...existingRule.attributes,
+ actions: [
+ {
+ frequency: {
+ notifyWhen: 'onActiveAlert' as const,
+ summary: false,
+ throttle: null,
+ },
+ group: 'default',
+ params: {},
+ actionRef: 'action_0',
+ actionTypeId: 'test-1',
+ uuid: '222',
+ },
+ {
+ params: {},
+ actionRef: 'system_action:system_action-id',
+ actionTypeId: 'test-2',
+ uuid: '222',
+ },
+ ],
+ },
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ ],
+ },
+ ],
+ });
+
+ actionsClient.getBulk.mockResolvedValue([
+ {
+ id: '1',
+ actionTypeId: 'test-1',
+ config: {},
+ isMissingSecrets: false,
+ name: 'test default connector',
+ isPreconfigured: false,
+ isDeprecated: false,
+ isSystemAction: false,
+ },
+ {
+ id: 'system_action-id',
+ actionTypeId: 'test-2',
+ config: {},
+ isMissingSecrets: false,
+ name: 'system action connector',
+ isPreconfigured: false,
+ isDeprecated: false,
+ isSystemAction: true,
+ },
+ ]);
+
+ actionsAuthorization.ensureAuthorized.mockRejectedValueOnce(
+ new Error('Unauthorized to execute actions')
+ );
+
+ const res = await rulesClient.bulkEdit({
+ filter: '',
+ operations: [
+ {
+ field: 'actions',
+ operation: 'add',
+ value: [defaultAction, systemAction],
+ },
+ ],
+ });
+
+ expect(res.rules.length).toBe(0);
+ expect(res.skipped.length).toBe(0);
+ expect(res.total).toBe(1);
+
+ expect(res.errors).toEqual([
+ {
+ message: 'Unauthorized to execute actions',
+ rule: {
+ id: '1',
+ name: 'my rule name',
+ },
+ },
+ ]);
+ });
});
describe('index pattern operations', () => {
diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts
index 53508a2de0ceb..e31b1ffa8c1dc 100644
--- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts
+++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts
@@ -15,7 +15,7 @@ import {
SavedObjectsFindResult,
SavedObjectsUpdateResponse,
} from '@kbn/core/server';
-import { validateSystemActions } from '../../../../lib/validate_system_actions';
+import { validateAndAuthorizeSystemActions } from '../../../../lib/validate_authorize_system_actions';
import { RuleAction, RuleSystemAction } from '../../../../../common';
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
import { BulkActionSkipResult } from '../../../../../common/bulk_edit';
@@ -682,10 +682,12 @@ async function getUpdatedAttributesFromOperations({
value: [...genActions, ...genSystemActions],
};
- await validateSystemActions({
+ await validateAndAuthorizeSystemActions({
actionsClient,
+ actionsAuthorization: context.actionsAuthorization,
connectorAdapterRegistry: context.connectorAdapterRegistry,
systemActions: genSystemActions,
+ rule: { consumer: updatedRule.consumer },
});
try {
diff --git a/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts
index 7d1fe07d8a7a4..50f5abb5d3d73 100644
--- a/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts
+++ b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts
@@ -4353,5 +4353,35 @@ describe('create()', () => {
`[Error: Cannot use the same system action twice]`
);
});
+
+ test('should throw an error if the user does not have privileges to execute the action', async () => {
+ actionsAuthorization.ensureAuthorized.mockRejectedValueOnce(
+ new Error('Unauthorized to execute actions')
+ );
+
+ const data = getMockData({
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ systemActions: [
+ {
+ id: 'system_action-id',
+ params: {
+ foo: 'test',
+ },
+ },
+ ],
+ });
+
+ await expect(() => rulesClient.create({ data })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to execute actions]`
+ );
+ });
});
});
diff --git a/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.ts
index 7de8c7effa8f3..e86744b421bb8 100644
--- a/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.ts
+++ b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.ts
@@ -8,7 +8,7 @@ import Semver from 'semver';
import Boom from '@hapi/boom';
import { SavedObject, SavedObjectsUtils } from '@kbn/core/server';
import { withSpan } from '@kbn/apm-utils';
-import { validateSystemActions } from '../../../../lib/validate_system_actions';
+import { validateAndAuthorizeSystemActions } from '../../../../lib/validate_authorize_system_actions';
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
import { parseDuration, getRuleCircuitBreakerErrorMessage } from '../../../../../common';
import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization';
@@ -98,7 +98,7 @@ export async function createRule(
}
try {
- await withSpan({ name: 'authorization.ensureAuthorized', type: 'rules' }, () =>
+ await withSpan({ name: 'authorization.ensureAuthorized', type: 'rules' }, async () =>
context.authorization.ensureAuthorized({
ruleTypeId: data.alertTypeId,
consumer: data.consumer,
@@ -149,11 +149,13 @@ export async function createRule(
validateActions(context, ruleType, data, allowMissingConnectorSecrets)
);
- await withSpan({ name: 'validateSystemActions', type: 'rules' }, () =>
- validateSystemActions({
+ await withSpan({ name: 'validateAndAuthorizeSystemActions', type: 'rules' }, () =>
+ validateAndAuthorizeSystemActions({
actionsClient,
+ actionsAuthorization: context.actionsAuthorization,
connectorAdapterRegistry: context.connectorAdapterRegistry,
systemActions: data.systemActions,
+ rule: { consumer: data.consumer },
})
);
diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts
index 882d3bd103531..da2774e263b58 100644
--- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts
+++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts
@@ -750,6 +750,49 @@ describe('AlertingAuthorization', () => {
`"Unauthorized by \\"myOtherApp\\" to create \\"myType\\" alert"`
);
});
+
+ test('checks additional privileges correctly', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction<
+ ReturnType
+ > = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ const alertAuthorization = new AlertingAuthorization({
+ request,
+ authorization,
+ ruleTypeRegistry,
+ features,
+ getSpace,
+ getSpaceId,
+ });
+
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: true,
+ privileges: { kibana: [] },
+ });
+
+ await alertAuthorization.ensureAuthorized({
+ ruleTypeId: 'myType',
+ consumer: 'myApp',
+ operation: WriteOperations.Create,
+ entity: AlertingAuthorizationEntity.Rule,
+ additionalPrivileges: ['test/create'],
+ });
+
+ expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType');
+
+ expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1);
+ expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
+ 'myType',
+ 'myApp',
+ 'rule',
+ 'create'
+ );
+ expect(checkPrivileges).toHaveBeenCalledWith({
+ kibana: [mockAuthorizationAction('myType', 'myApp', 'rule', 'create'), 'test/create'],
+ });
+ });
});
describe('getFindAuthorizationFilter', () => {
diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts
index 68edc6667aeed..f5bf6ee049fbf 100644
--- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts
+++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts
@@ -62,6 +62,7 @@ export interface EnsureAuthorizedOpts {
consumer: string;
operation: ReadOperations | WriteOperations;
entity: AlertingAuthorizationEntity;
+ additionalPrivileges?: string[];
}
interface HasPrivileges {
@@ -172,6 +173,7 @@ export class AlertingAuthorization {
consumer: legacyConsumer,
operation,
entity,
+ additionalPrivileges = [],
}: EnsureAuthorizedOpts) {
const { authorization } = this;
const ruleType = this.ruleTypeRegistry.get(ruleTypeId);
@@ -186,7 +188,10 @@ export class AlertingAuthorization {
const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request);
const { hasAllRequested } = await checkPrivileges({
- kibana: [authorization.actions.alerting.get(ruleTypeId, consumer, entity, operation)],
+ kibana: [
+ authorization.actions.alerting.get(ruleTypeId, consumer, entity, operation),
+ ...additionalPrivileges,
+ ],
});
if (!isAvailableConsumer) {
diff --git a/x-pack/plugins/alerting/server/connector_adapters/get_system_action_kibana_privileges.test.ts b/x-pack/plugins/alerting/server/connector_adapters/get_system_action_kibana_privileges.test.ts
new file mode 100644
index 0000000000000..9f5d053cbc2b3
--- /dev/null
+++ b/x-pack/plugins/alerting/server/connector_adapters/get_system_action_kibana_privileges.test.ts
@@ -0,0 +1,102 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+import { ConnectorAdapterRegistry } from './connector_adapter_registry';
+import { getSystemActionKibanaPrivileges } from './get_system_action_kibana_privileges';
+import type { ConnectorAdapter } from './types';
+
+describe('getSystemActionKibanaPrivileges', () => {
+ const connectorAdapter: ConnectorAdapter = {
+ connectorTypeId: '.test',
+ ruleActionParamsSchema: schema.object({ foo: schema.string() }),
+ buildActionParams: jest.fn(),
+ getKibanaPrivileges: (args) => [`my-priv:${args.consumer}`],
+ };
+
+ const systemActions = [
+ { id: 'my-id', actionTypeId: '.test', params: {} },
+ { id: 'my-id-2', actionTypeId: '.test-2', params: {} },
+ ];
+
+ let registry: ConnectorAdapterRegistry;
+
+ beforeEach(() => {
+ registry = new ConnectorAdapterRegistry();
+ registry.register(connectorAdapter);
+
+ registry.register({
+ ...connectorAdapter,
+ connectorTypeId: '.test-2',
+ getKibanaPrivileges: (args) => [`my-priv-2:${args.consumer}`],
+ });
+
+ registry.register({
+ ...connectorAdapter,
+ connectorTypeId: '.no-priv',
+ });
+ });
+
+ it('should return an empty array if systemActions are empty', () => {
+ const privileges = getSystemActionKibanaPrivileges({
+ connectorAdapterRegistry: registry,
+ systemActions: [],
+ rule: { consumer: 'stackAlerts' },
+ });
+
+ expect(privileges).toEqual([]);
+ });
+
+ it('should return an empty array if systemActions are not defined', () => {
+ const privileges = getSystemActionKibanaPrivileges({
+ connectorAdapterRegistry: registry,
+ rule: { consumer: 'stackAlerts' },
+ });
+
+ expect(privileges).toEqual([]);
+ });
+
+ it('should return the privileges correctly', () => {
+ const privileges = getSystemActionKibanaPrivileges({
+ connectorAdapterRegistry: registry,
+ systemActions,
+ rule: { consumer: 'stackAlerts' },
+ });
+
+ expect(privileges).toEqual(['my-priv:stackAlerts', 'my-priv-2:stackAlerts']);
+ });
+
+ it('should return the privileges correctly with system actions without connector adapter', () => {
+ const privileges = getSystemActionKibanaPrivileges({
+ connectorAdapterRegistry: registry,
+ systemActions: [...systemActions, { id: 'my-id-2', actionTypeId: '.not-valid', params: {} }],
+ rule: { consumer: 'stackAlerts' },
+ });
+
+ expect(privileges).toEqual(['my-priv:stackAlerts', 'my-priv-2:stackAlerts']);
+ });
+
+ it('should return the privileges correctly with system actions without getKibanaPrivileges defined', () => {
+ const privileges = getSystemActionKibanaPrivileges({
+ connectorAdapterRegistry: registry,
+ systemActions: [...systemActions, { id: 'my-id-2', actionTypeId: '.no-priv', params: {} }],
+ rule: { consumer: 'stackAlerts' },
+ });
+
+ expect(privileges).toEqual(['my-priv:stackAlerts', 'my-priv-2:stackAlerts']);
+ });
+
+ it('should not return duplicated privileges', () => {
+ const privileges = getSystemActionKibanaPrivileges({
+ connectorAdapterRegistry: registry,
+ systemActions: [systemActions[0], systemActions[0]],
+ rule: { consumer: 'stackAlerts' },
+ });
+
+ expect(privileges).toEqual(['my-priv:stackAlerts']);
+ });
+});
diff --git a/x-pack/plugins/alerting/server/connector_adapters/get_system_action_kibana_privileges.ts b/x-pack/plugins/alerting/server/connector_adapters/get_system_action_kibana_privileges.ts
new file mode 100644
index 0000000000000..2ffaaf5ead318
--- /dev/null
+++ b/x-pack/plugins/alerting/server/connector_adapters/get_system_action_kibana_privileges.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { RuleSystemAction } from '../types';
+import { ConnectorAdapterRegistry } from './connector_adapter_registry';
+
+interface Args {
+ connectorAdapterRegistry: ConnectorAdapterRegistry;
+ rule: { consumer: string };
+ systemActions?: RuleSystemAction[];
+}
+
+export const getSystemActionKibanaPrivileges = ({
+ connectorAdapterRegistry,
+ systemActions = [],
+ rule,
+}: Args): string[] => {
+ const kibanaPrivileges = systemActions
+ .filter((action) => connectorAdapterRegistry.has(action.actionTypeId))
+ .map((action) => connectorAdapterRegistry.get(action.actionTypeId))
+ .map((adapter) => adapter.getKibanaPrivileges?.({ consumer: rule.consumer }) ?? [])
+ .flat();
+
+ return Array.from(new Set(kibanaPrivileges));
+};
diff --git a/x-pack/plugins/alerting/server/connector_adapters/types.ts b/x-pack/plugins/alerting/server/connector_adapters/types.ts
index e2bd2d7ed2c93..4a9ab393270b7 100644
--- a/x-pack/plugins/alerting/server/connector_adapters/types.ts
+++ b/x-pack/plugins/alerting/server/connector_adapters/types.ts
@@ -38,4 +38,5 @@ export interface ConnectorAdapter<
*/
ruleActionParamsSchema: ObjectType;
buildActionParams: (args: BuildActionParamsArgs) => ConnectorParams;
+ getKibanaPrivileges?: ({ consumer }: { consumer: string }) => string[];
}
diff --git a/x-pack/plugins/alerting/server/lib/validate_system_actions.test.ts b/x-pack/plugins/alerting/server/lib/validate_authorize_system_actions.test.ts
similarity index 65%
rename from x-pack/plugins/alerting/server/lib/validate_system_actions.test.ts
rename to x-pack/plugins/alerting/server/lib/validate_authorize_system_actions.test.ts
index 7cc7b4041aa08..f69213b4f3bf2 100644
--- a/x-pack/plugins/alerting/server/lib/validate_system_actions.test.ts
+++ b/x-pack/plugins/alerting/server/lib/validate_authorize_system_actions.test.ts
@@ -5,16 +5,16 @@
* 2.0.
*/
-import { ActionsClient } from '@kbn/actions-plugin/server';
-import { actionsClientMock } from '@kbn/actions-plugin/server/mocks';
+import { ActionsAuthorization, ActionsClient } from '@kbn/actions-plugin/server';
+import { actionsAuthorizationMock, actionsClientMock } from '@kbn/actions-plugin/server/mocks';
import { schema } from '@kbn/config-schema';
import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry';
import { ConnectorAdapter } from '../connector_adapters/types';
import { NormalizedSystemAction } from '../rules_client';
import { RuleSystemAction } from '../types';
-import { validateSystemActions } from './validate_system_actions';
+import { validateAndAuthorizeSystemActions } from './validate_authorize_system_actions';
-describe('validateSystemActionsWithoutRuleTypeId', () => {
+describe('validateAndAuthorizeSystemActions', () => {
const connectorAdapter: ConnectorAdapter = {
connectorTypeId: '.test',
ruleActionParamsSchema: schema.object({ foo: schema.string() }),
@@ -23,10 +23,13 @@ describe('validateSystemActionsWithoutRuleTypeId', () => {
let registry: ConnectorAdapterRegistry;
let actionsClient: jest.Mocked;
+ let actionsAuthorization: jest.Mocked;
beforeEach(() => {
registry = new ConnectorAdapterRegistry();
actionsClient = actionsClientMock.create();
+ actionsAuthorization = actionsAuthorizationMock.create();
+
actionsClient.getBulk.mockResolvedValue([
{
id: 'system_action-id',
@@ -42,10 +45,12 @@ describe('validateSystemActionsWithoutRuleTypeId', () => {
});
it('should not validate with empty system actions', async () => {
- const res = await validateSystemActions({
+ const res = await validateAndAuthorizeSystemActions({
connectorAdapterRegistry: registry,
systemActions: [],
actionsClient,
+ actionsAuthorization,
+ rule: { consumer: 'stackAlerts' },
});
expect(res).toBe(undefined);
@@ -68,10 +73,12 @@ describe('validateSystemActionsWithoutRuleTypeId', () => {
actionsClient.isSystemAction.mockReturnValue(false);
await expect(() =>
- validateSystemActions({
+ validateAndAuthorizeSystemActions({
connectorAdapterRegistry: registry,
systemActions,
actionsClient,
+ actionsAuthorization,
+ rule: { consumer: 'stackAlerts' },
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"Action not-exist is not a system action"`);
});
@@ -91,10 +98,12 @@ describe('validateSystemActionsWithoutRuleTypeId', () => {
actionsClient.isSystemAction.mockReturnValue(true);
await expect(() =>
- validateSystemActions({
+ validateAndAuthorizeSystemActions({
connectorAdapterRegistry: registry,
systemActions,
actionsClient,
+ actionsAuthorization,
+ rule: { consumer: 'stackAlerts' },
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"Action not-exist is not a system action"`);
});
@@ -114,10 +123,12 @@ describe('validateSystemActionsWithoutRuleTypeId', () => {
actionsClient.isSystemAction.mockReturnValue(true);
await expect(() =>
- validateSystemActions({
+ validateAndAuthorizeSystemActions({
connectorAdapterRegistry: registry,
systemActions,
actionsClient,
+ actionsAuthorization,
+ rule: { consumer: 'stackAlerts' },
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Invalid system action params. System action type: .test - [foo]: expected value of type [string] but got [undefined]"`
@@ -145,10 +156,12 @@ describe('validateSystemActionsWithoutRuleTypeId', () => {
actionsClient.isSystemAction.mockReturnValue(false);
await expect(() =>
- validateSystemActions({
+ validateAndAuthorizeSystemActions({
connectorAdapterRegistry: registry,
systemActions,
actionsClient,
+ actionsAuthorization,
+ rule: { consumer: 'stackAlerts' },
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"Cannot use the same system action twice"`);
});
@@ -195,10 +208,12 @@ describe('validateSystemActionsWithoutRuleTypeId', () => {
actionsClient.isSystemAction.mockReturnValue(true);
- const res = await validateSystemActions({
+ const res = await validateAndAuthorizeSystemActions({
connectorAdapterRegistry: registry,
systemActions,
actionsClient,
+ actionsAuthorization,
+ rule: { consumer: 'stackAlerts' },
});
expect(res).toBe(undefined);
@@ -207,5 +222,71 @@ describe('validateSystemActionsWithoutRuleTypeId', () => {
ids: ['system_action-id', 'system_action-id-2'],
throwIfSystemAction: false,
});
+
+ expect(actionsAuthorization.ensureAuthorized).toBeCalledWith({
+ operation: 'execute',
+ additionalPrivileges: [],
+ });
+ });
+
+ it('should call ensureAuthorized correctly', async () => {
+ const systemActions: Array = [
+ {
+ id: 'system_action-id',
+ uuid: '123',
+ params: { foo: 'test' },
+ },
+ {
+ id: 'system_action-id-2',
+ uuid: '123',
+ params: { foo: 'test' },
+ actionTypeId: '.test-2',
+ },
+ ];
+
+ actionsClient.getBulk.mockResolvedValue([
+ {
+ id: 'system_action-id',
+ actionTypeId: '.test',
+ config: {},
+ isMissingSecrets: false,
+ name: 'system action connector',
+ isPreconfigured: false,
+ isDeprecated: false,
+ isSystemAction: true,
+ },
+ {
+ id: 'system_action-id-2',
+ actionTypeId: '.test-2',
+ config: {},
+ isMissingSecrets: false,
+ name: 'system action connector 2',
+ isPreconfigured: false,
+ isDeprecated: false,
+ isSystemAction: true,
+ },
+ ]);
+
+ registry.register(connectorAdapter);
+ registry.register({
+ ...connectorAdapter,
+ connectorTypeId: '.test-2',
+ getKibanaPrivileges: (args) => [`my-priv-2:${args.consumer}`],
+ });
+
+ actionsClient.isSystemAction.mockReturnValue(true);
+
+ await validateAndAuthorizeSystemActions({
+ connectorAdapterRegistry: registry,
+ systemActions,
+ actionsClient,
+ actionsAuthorization,
+ rule: { consumer: 'stackAlerts' },
+ });
+
+ expect(actionsAuthorization.ensureAuthorized).toBeCalledWith({
+ operation: 'execute',
+ additionalPrivileges: ['my-priv-2:stackAlerts'],
+ });
});
});
diff --git a/x-pack/plugins/alerting/server/lib/validate_system_actions.ts b/x-pack/plugins/alerting/server/lib/validate_authorize_system_actions.ts
similarity index 77%
rename from x-pack/plugins/alerting/server/lib/validate_system_actions.ts
rename to x-pack/plugins/alerting/server/lib/validate_authorize_system_actions.ts
index 190b695efffd7..22ab3f32d3d7a 100644
--- a/x-pack/plugins/alerting/server/lib/validate_system_actions.ts
+++ b/x-pack/plugins/alerting/server/lib/validate_authorize_system_actions.ts
@@ -6,20 +6,25 @@
*/
import Boom from '@hapi/boom';
-import { ActionsClient } from '@kbn/actions-plugin/server';
+import { ActionsAuthorization, ActionsClient } from '@kbn/actions-plugin/server';
import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry';
+import { getSystemActionKibanaPrivileges } from '../connector_adapters/get_system_action_kibana_privileges';
import { bulkValidateConnectorAdapterActionParams } from '../connector_adapters/validate_rule_action_params';
import { NormalizedSystemAction } from '../rules_client';
import { RuleSystemAction } from '../types';
interface Params {
actionsClient: ActionsClient;
+ actionsAuthorization: ActionsAuthorization;
connectorAdapterRegistry: ConnectorAdapterRegistry;
systemActions: Array;
+ rule: { consumer: string };
}
-export const validateSystemActions = async ({
+export const validateAndAuthorizeSystemActions = async ({
actionsClient,
connectorAdapterRegistry,
+ actionsAuthorization,
+ rule,
systemActions = [],
}: Params) => {
if (systemActions.length === 0) {
@@ -64,4 +69,12 @@ export const validateSystemActions = async ({
connectorAdapterRegistry,
actions: systemActionsWithActionTypeId,
});
+
+ const additionalPrivileges = getSystemActionKibanaPrivileges({
+ connectorAdapterRegistry,
+ systemActions: systemActionsWithActionTypeId,
+ rule: { consumer: rule.consumer },
+ });
+
+ await actionsAuthorization.ensureAuthorized({ operation: 'execute', additionalPrivileges });
};
diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update.ts b/x-pack/plugins/alerting/server/rules_client/methods/update.ts
index 1b54dfcb18d94..886f112ba2984 100644
--- a/x-pack/plugins/alerting/server/rules_client/methods/update.ts
+++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts
@@ -37,7 +37,7 @@ import {
validateScheduleLimit,
ValidateScheduleLimitResult,
} from '../../application/rule/methods/get_schedule_frequency';
-import { validateSystemActions } from '../../lib/validate_system_actions';
+import { validateAndAuthorizeSystemActions } from '../../lib/validate_authorize_system_actions';
import { transformRawActionsToDomainActions } from '../../application/rule/transforms';
import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';
import { transformRawActionsToDomainSystemActions } from '../../application/rule/transforms/transform_raw_actions_to_domain_actions';
@@ -245,10 +245,12 @@ async function updateAlert(
// Validate
const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate.params);
await validateActions(context, ruleType, data, allowMissingConnectorSecrets);
- await validateSystemActions({
+ await validateAndAuthorizeSystemActions({
actionsClient,
+ actionsAuthorization: context.actionsAuthorization,
connectorAdapterRegistry: context.connectorAdapterRegistry,
systemActions: data.systemActions,
+ rule: { consumer: currentRule.attributes.consumer },
});
// Throw error if schedule interval is less than the minimum and we are enforcing it
diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
index d66b17b5ff6b6..4daf489091d2c 100644
--- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts
@@ -3837,5 +3837,42 @@ describe('update()', () => {
`[Error: Error validating actions - [actions.0.group]: expected value of type [string] but got [undefined]]`
);
});
+
+ test('should throw an error if the user does not have privileges to execute the action', async () => {
+ actionsAuthorization.ensureAuthorized.mockRejectedValueOnce(
+ new Error('Unauthorized to execute actions')
+ );
+
+ await expect(() =>
+ rulesClient.update({
+ id: '1',
+ data: {
+ schedule: { interval: '1m' },
+ name: 'abc',
+ tags: ['foo'],
+ params: {
+ bar: true,
+ },
+ throttle: null,
+ notifyWhen: 'onActiveAlert',
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ systemActions: [
+ {
+ id: 'system_action-id',
+ params: {},
+ },
+ ],
+ },
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute actions]`);
+ });
});
});
diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts
index 27df735ad73dc..f6903da3c62bc 100644
--- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts
+++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts
@@ -448,7 +448,13 @@ function getSystemActionTypeWithKibanaPrivileges() {
getKibanaPrivileges: () => ['cases:cases/createCase'],
validate: {
params: {
- schema: schema.any(),
+ /**
+ * Adapter: x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts
+ */
+ schema: schema.object({
+ index: schema.maybe(schema.string()),
+ reference: schema.maybe(schema.string()),
+ }),
},
config: {
schema: schema.any(),
diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts
index 41526e0949de3..af68d197acd3d 100644
--- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts
+++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts
@@ -33,5 +33,18 @@ export function defineConnectorAdapters(
},
};
+ const systemActionConnectorAdapterWithKibanaPrivileges: ConnectorAdapter = {
+ connectorTypeId: 'test.system-action-kibana-privileges',
+ ruleActionParamsSchema: schema.object({
+ index: schema.maybe(schema.string()),
+ reference: schema.maybe(schema.string()),
+ }),
+ buildActionParams: ({ alerts, rule, params, spaceId, ruleUrl }) => {
+ return params;
+ },
+ getKibanaPrivileges: () => ['cases:cases/createCase'],
+ };
+
alerting.registerConnectorAdapter(systemActionConnectorAdapter);
+ alerting.registerConnectorAdapter(systemActionConnectorAdapterWithKibanaPrivileges);
}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts
index b234a821834f4..10359b53f0355 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts
@@ -7,7 +7,8 @@
import expect from '@kbn/expect';
import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server';
-import { UserAtSpaceScenarios } from '../../../scenarios';
+import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
+import { systemActionScenario, UserAtSpaceScenarios } from '../../../scenarios';
import {
checkAAD,
getTestRuleData,
@@ -37,7 +38,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
return scheduledTask._source!;
}
- for (const scenario of UserAtSpaceScenarios) {
+ for (const scenario of [...UserAtSpaceScenarios, systemActionScenario]) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should handle create alert request appropriately', async () => {
@@ -90,6 +91,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'superuser at space1':
case 'space_1_all at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'rule', 'alerting');
expect(response.body).to.eql({
@@ -187,6 +189,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
break;
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'rule', 'alerting');
break;
@@ -226,6 +229,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'space_1_all_alerts_none_actions at space1':
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'rule', 'alerting');
break;
@@ -268,6 +272,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'space_1_all_alerts_none_actions at space1':
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'rule', 'alerting');
break;
@@ -296,6 +301,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
case 'superuser at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
@@ -334,6 +340,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'rule', 'alerting');
expect(response.body.scheduled_task_id).to.eql(undefined);
@@ -369,6 +376,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.name).to.eql(' leading and trailing whitespace ');
objectRemover.add(space.id, response.body.id, 'rule', 'alerting');
@@ -397,6 +405,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
case 'superuser at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -424,6 +433,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -462,6 +472,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -490,6 +501,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
error: 'Bad Request',
@@ -517,6 +529,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
error: 'Bad Request',
@@ -528,6 +541,65 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
+
+ it('should handle create alert request appropriately with system actions', async () => {
+ const connectorId = 'system-connector-test.system-action-kibana-privileges';
+ const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`;
+
+ const systemActionWithKibanaPrivileges = {
+ id: connectorId,
+ group: 'default',
+ params: { index: ES_TEST_INDEX_NAME, reference },
+ };
+
+ const response = await supertestWithoutAuth
+ .post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send(
+ getTestRuleData({
+ actions: [systemActionWithKibanaPrivileges],
+ })
+ );
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'global_read at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getUnauthorizedErrorMessage('create', 'test.noop', 'alertsFixture'),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: 'Unauthorized to execute actions',
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: 'Unauthorized to get actions',
+ statusCode: 403,
+ });
+ break;
+
+ case 'superuser at space1':
+ case 'system_actions at space1':
+ expect(response.statusCode).to.eql(200);
+ objectRemover.add(space.id, response.body.id, 'rule', 'alerting');
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
});
}
});
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts
index 3771039e392c9..feba08b7d91e7 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts
@@ -11,7 +11,8 @@ import { RuleNotifyWhen, RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/ser
import { RawRule } from '@kbn/alerting-plugin/server/types';
import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import { SavedObject } from '@kbn/core-saved-objects-api-server';
-import { SuperuserAtSpace1, UserAtSpaceScenarios } from '../../../scenarios';
+import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
+import { SuperuserAtSpace1, systemActionScenario, UserAtSpaceScenarios } from '../../../scenarios';
import {
checkAAD,
getUrlPrefix,
@@ -41,7 +42,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
after(() => objectRemover.removeAll());
- for (const scenario of UserAtSpaceScenarios) {
+ for (const scenario of [...UserAtSpaceScenarios, systemActionScenario]) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should handle update alert request appropriately', async () => {
@@ -108,6 +109,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'superuser at space1':
case 'space_1_all at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
...updatedData,
@@ -209,6 +211,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
break;
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
...updatedData,
@@ -301,6 +304,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all_alerts_none_actions at space1':
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
...updatedData,
@@ -400,6 +404,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
break;
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
...updatedData,
@@ -497,6 +502,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
...updatedData,
@@ -580,6 +586,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.name).to.eql(' leading and trailing whitespace ');
break;
@@ -621,6 +628,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
case 'superuser at space1':
+ case 'system_actions at space1':
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
@@ -665,6 +673,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -692,6 +701,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -748,6 +758,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -782,6 +793,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -845,6 +857,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
await retry.try(async () => {
const alertTask = (await getAlertingTaskById(createdAlert.scheduled_task_id))
@@ -918,6 +931,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
await retry.try(async () => {
const alertTask = (await getAlertingTaskById(createdAlert.scheduled_task_id))
@@ -982,6 +996,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
await retry.try(async () => {
const alertTask = (await getAlertingTaskById(createdAlert.scheduled_task_id))
@@ -994,6 +1009,79 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
+
+ it('should handle create alert request appropriately with system actions', async () => {
+ const connectorId = 'system-connector-test.system-action-kibana-privileges';
+ const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`;
+
+ const systemActionWithKibanaPrivileges = {
+ id: connectorId,
+ group: 'default',
+ params: { index: ES_TEST_INDEX_NAME, reference },
+ };
+
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
+ .set('kbn-xsrf', 'foo')
+ .send(getTestRuleData())
+ .expect(200);
+
+ objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting');
+
+ const updatedData = {
+ name: 'bcd',
+ tags: ['bar'],
+ params: {
+ foo: true,
+ },
+ schedule: { interval: '12s' },
+ actions: [systemActionWithKibanaPrivileges],
+ throttle: '1m',
+ notify_when: 'onThrottleInterval',
+ };
+
+ const response = await supertestWithoutAuth
+ .put(`${getUrlPrefix(space.id)}/api/alerting/rule/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send(updatedData);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'global_read at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: 'Unauthorized to execute actions',
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'system_actions at space1':
+ expect(response.statusCode).to.eql(200);
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
});
}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts
index f09ab4b2fe66d..1082a6741ec0d 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts
@@ -11,7 +11,8 @@ import { RuleNotifyWhen, SanitizedRule } from '@kbn/alerting-plugin/common';
import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server';
import { RawRule } from '@kbn/alerting-plugin/server/types';
import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
-import { SuperuserAtSpace1, UserAtSpaceScenarios } from '../../../scenarios';
+import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
+import { SuperuserAtSpace1, systemActionScenario, UserAtSpaceScenarios } from '../../../scenarios';
import {
checkAAD,
getUrlPrefix,
@@ -32,7 +33,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
after(() => objectRemover.removeAll());
- for (const scenario of UserAtSpaceScenarios) {
+ for (const scenario of [...UserAtSpaceScenarios, systemActionScenario]) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should handle bulk edit of rules appropriately', async () => {
@@ -114,6 +115,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'superuser at space1':
case 'space_1_all at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.body.rules[0].actions).to.eql([
{
id: createdAction.id,
@@ -191,6 +193,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'superuser at space1':
case 'space_1_all at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
response.body.rules.forEach((rule: SanitizedRule) =>
expect(rule.tags).to.eql([`multiple-rules-edit-${scenario.id}`, 'tag-A'])
);
@@ -264,6 +267,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
break;
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.body.rules[0].tags).to.eql(['foo', 'tag-A', 'tag-B']);
expect(response.statusCode).to.eql(200);
break;
@@ -327,6 +331,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all_alerts_none_actions at space1':
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.body.rules[0].tags).to.eql(['foo', 'tag-A', 'tag-B']);
expect(response.statusCode).to.eql(200);
@@ -394,6 +399,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
break;
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.body.rules[0].tags).to.eql(['foo', 'tag-A', 'tag-B']);
expect(response.statusCode).to.eql(200);
@@ -428,6 +434,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'superuser at space1':
case 'space_1_all at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.body).to.eql({
statusCode: 400,
error: 'Bad Request',
@@ -466,6 +473,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'superuser at space1':
case 'space_1_all at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.body).to.eql({
statusCode: 400,
error: 'Bad Request',
@@ -504,6 +512,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'superuser at space1':
case 'space_1_all at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.body).to.eql({
statusCode: 400,
error: 'Bad Request',
@@ -543,6 +552,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'superuser at space1':
case 'space_1_all at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ case 'system_actions at space1':
expect(response.body).to.eql({
error: 'Bad Request',
message:
@@ -582,6 +592,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'superuser at space1':
case 'global_read at space1':
+ case 'system_actions at space1':
expect(response.body).to.eql({ rules: [], skipped: [], errors: [], total: 0 });
expect(response.statusCode).to.eql(200);
break;
@@ -601,6 +612,90 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
+
+ it('should handle bulk edit rules with system actions', async () => {
+ const connectorId = 'system-connector-test.system-action-kibana-privileges';
+ const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`;
+
+ const { body: createdRule } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
+ .set('kbn-xsrf', 'foo')
+ .send(getTestRuleData())
+ .expect(200);
+
+ objectRemover.add(space.id, createdRule.id, 'rule', 'alerting');
+
+ const response = await supertestWithoutAuth
+ .post(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_edit`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send({
+ ids: [createdRule.id],
+ operations: [
+ {
+ operation: 'add',
+ field: 'actions',
+ value: [
+ {
+ id: connectorId,
+ params: { index: ES_TEST_INDEX_NAME, reference },
+ },
+ ],
+ },
+ ],
+ });
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: 'Unauthorized to find rules for any rule types',
+ statusCode: 403,
+ });
+
+ break;
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getUnauthorizedErrorMessage('bulkEdit', 'test.noop', 'alertsFixture'),
+ statusCode: 403,
+ });
+
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body.errors).to.eql([
+ {
+ message: 'Unauthorized to execute actions',
+ rule: { id: createdRule.id, name: createdRule.name },
+ },
+ ]);
+
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body.errors).to.eql([
+ {
+ message: 'Unauthorized to get actions',
+ rule: { id: createdRule.id, name: createdRule.name },
+ },
+ ]);
+
+ break;
+ case 'superuser at space1':
+ case 'system_actions at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body.errors).to.eql([]);
+
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
});
}
From f26b368180a414c959e48f7e5edac38dcc6c52e8 Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Wed, 10 Apr 2024 17:15:07 +0200
Subject: [PATCH 18/55] [HTTP] Update static asset headers to include `public`
and `immutable` (#180378)
## Summary
Adds the `public` and `immutable` ~header~ directives to static assets
registered in via the `core_app.ts` logic (se [mdn
docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
for more info). This will ensure that assets are stored in a share-safe
cache and that for the "fresh" duration (1 year) the browser won't reach
out to Kibana server for the asset again.
## Notes
* This does not include all static assets registered via
`registerStaticDir`
* We could consider updating [this
logic](https://github.com/elastic/kibana/blob/29e3ccd6262f4b05e469ac63e357639d66fd5483/packages/core/apps/core-apps-server-internal/src/core_app.ts#L274),
but those assets are not cache bustable, so we'd need to selectively add
the header to the path prefixed with static asset prefix, can do in this
PR or follow up
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../dynamic_asset_response.test.ts | 87 +++++++++++++++++++
.../bundle_routes/dynamic_asset_response.ts | 4 +-
.../core-apps-server-internal/tsconfig.json | 1 +
.../core_app/bundle_routes.test.ts | 2 +-
4 files changed, 92 insertions(+), 2 deletions(-)
create mode 100644 packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.test.ts
diff --git a/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.test.ts b/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.test.ts
new file mode 100644
index 0000000000000..dbf4b806be42c
--- /dev/null
+++ b/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.test.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import mockFs from 'mock-fs';
+import { kibanaResponseFactory } from '@kbn/core-http-router-server-internal';
+import { createDynamicAssetHandler } from './dynamic_asset_response';
+
+function getHandler(args?: Partial[0]>) {
+ return createDynamicAssetHandler({
+ bundlesPath: '/test',
+ publicPath: '/public',
+ fileHashCache: {
+ get: jest.fn(),
+ set: jest.fn(),
+ del: jest.fn(),
+ },
+ isDist: true,
+ ...args,
+ });
+}
+
+afterEach(() => {
+ mockFs.restore();
+});
+it('returns 403 if the path requested does not match bundle path', async () => {
+ const handler = getHandler();
+ const result = await handler(
+ {} as any,
+ { params: { path: '/non-existent/abc.js' }, headers: { 'accept-encoding': '*' } } as any,
+ kibanaResponseFactory
+ );
+ expect(result.status).toBe(403);
+});
+
+it('returns 404 if the file does not exist', async () => {
+ const handler = getHandler();
+ mockFs({}); // no files
+ const filePath = '/test/abc.js';
+ const result = await handler(
+ {} as any,
+ { params: { path: filePath }, headers: { 'accept-encoding': '*' } } as any,
+ kibanaResponseFactory
+ );
+ expect(result.status).toBe(404);
+});
+
+describe('headers', () => {
+ it('returns the expected headers', async () => {
+ const handler = getHandler();
+ const filePath = '/test/abc.js';
+ mockFs({
+ [filePath]: Buffer.from('test'),
+ });
+ const result = await handler(
+ {} as any,
+ { params: { path: filePath }, headers: { 'accept-encoding': 'br' } } as any,
+ kibanaResponseFactory
+ );
+ expect(result.options.headers).toEqual({
+ 'cache-control': 'public, max-age=31536000, immutable',
+ 'content-type': 'application/javascript; charset=utf-8',
+ });
+ });
+
+ it('returns the expected headers when not in dist mode', async () => {
+ const handler = getHandler({ isDist: false });
+ const filePath = '/test/abc.js';
+ mockFs({
+ [filePath]: Buffer.from('test'),
+ });
+ const result = await handler(
+ {} as any,
+ { params: { path: filePath }, headers: { 'accept-encoding': '*' } } as any,
+ kibanaResponseFactory
+ );
+ expect(result.options.headers).toEqual({
+ 'cache-control': 'must-revalidate',
+ 'content-type': 'application/javascript; charset=utf-8',
+ etag: expect.stringMatching(/^[a-f0-9]{40}-\/public/i),
+ });
+ });
+});
diff --git a/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.ts b/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.ts
index 219beced6ca65..b79e4d27e1b86 100644
--- a/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.ts
+++ b/packages/core/apps/core-apps-server-internal/src/bundle_routes/dynamic_asset_response.ts
@@ -75,7 +75,9 @@ export const createDynamicAssetHandler = ({
let headers: Record;
if (isDist) {
- headers = { 'cache-control': `max-age=${365 * DAY}` };
+ headers = {
+ 'cache-control': `public, max-age=${365 * DAY}, immutable`,
+ };
} else {
const stat = await fstat(fd);
const hash = await getFileHash(fileHashCache, path, stat, fd);
diff --git a/packages/core/apps/core-apps-server-internal/tsconfig.json b/packages/core/apps/core-apps-server-internal/tsconfig.json
index fc8aa9f25349c..35698e91f6ddb 100644
--- a/packages/core/apps/core-apps-server-internal/tsconfig.json
+++ b/packages/core/apps/core-apps-server-internal/tsconfig.json
@@ -33,6 +33,7 @@
"@kbn/core-ui-settings-server",
"@kbn/monaco",
"@kbn/core-http-server-internal",
+ "@kbn/core-http-router-server-internal",
],
"exclude": [
"target/**/*",
diff --git a/src/core/server/integration_tests/core_app/bundle_routes.test.ts b/src/core/server/integration_tests/core_app/bundle_routes.test.ts
index b53bc07a85492..58636e4fadb88 100644
--- a/src/core/server/integration_tests/core_app/bundle_routes.test.ts
+++ b/src/core/server/integration_tests/core_app/bundle_routes.test.ts
@@ -154,7 +154,7 @@ describe('bundle routes', () => {
.get(`/${buildHash}/bundles/plugin/foo/gzip_chunk.js`)
.expect(200);
- expect(response.get('cache-control')).toEqual('max-age=31536000');
+ expect(response.get('cache-control')).toEqual('public, max-age=31536000, immutable');
expect(response.get('etag')).toBeUndefined();
});
});
From fe507aa0b95e21c328da222354ed9ae6b7ee89d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?=
Date: Wed, 10 Apr 2024 17:18:23 +0200
Subject: [PATCH 19/55] [Obs AI Assistant] Update codeowners (#180385)
This updates the code owners from `obs-knowledge-team` to
`obs-ai-assistant` for all of the ai assistant plugins
---
.github/CODEOWNERS | 6 +++---
.../observability_ai_assistant/kibana.jsonc | 2 +-
.../observability_ai_assistant_app/kibana.jsonc | 4 ++--
.../observability_ai_assistant_management/kibana.jsonc | 2 +-
4 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 9e6bca0b54433..5cc46aad384a3 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -588,9 +588,9 @@ test/common/plugins/newsfeed @elastic/kibana-core
src/plugins/no_data_page @elastic/appex-sharedux
x-pack/plugins/notifications @elastic/appex-sharedux
packages/kbn-object-versioning @elastic/appex-sharedux
-x-pack/plugins/observability_solution/observability_ai_assistant_app @elastic/obs-knowledge-team
-x-pack/plugins/observability_solution/observability_ai_assistant_management @elastic/obs-knowledge-team
-x-pack/plugins/observability_solution/observability_ai_assistant @elastic/obs-knowledge-team
+x-pack/plugins/observability_solution/observability_ai_assistant_app @elastic/obs-ai-assistant
+x-pack/plugins/observability_solution/observability_ai_assistant_management @elastic/obs-ai-assistant
+x-pack/plugins/observability_solution/observability_ai_assistant @elastic/obs-ai-assistant
x-pack/packages/observability/alert_details @elastic/obs-ux-management-team
x-pack/packages/observability/alerting_test_data @elastic/obs-ux-management-team
x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/kibana.jsonc b/x-pack/plugins/observability_solution/observability_ai_assistant/kibana.jsonc
index 3f818b0f88dcc..39af4d91bc87b 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant/kibana.jsonc
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant/kibana.jsonc
@@ -1,7 +1,7 @@
{
"type": "plugin",
"id": "@kbn/observability-ai-assistant-plugin",
- "owner": "@elastic/obs-knowledge-team",
+ "owner": "@elastic/obs-ai-assistant",
"plugin": {
"id": "observabilityAIAssistant",
"server": true,
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc b/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc
index 713132ab2f7a7..f4aac7df8a389 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc
@@ -1,7 +1,7 @@
{
"type": "plugin",
"id": "@kbn/observability-ai-assistant-app-plugin",
- "owner": "@elastic/obs-knowledge-team",
+ "owner": "@elastic/obs-ai-assistant",
"plugin": {
"id": "observabilityAIAssistantApp",
"server": true,
@@ -23,7 +23,7 @@
"licensing",
"ml"
],
- "requiredBundles": [ "kibanaReact" ],
+ "requiredBundles": ["kibanaReact"],
"optionalPlugins": ["cloud"],
"extraPublicDirs": []
}
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc b/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc
index 7595eb93543b9..489d1ca7817f8 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc
@@ -1,7 +1,7 @@
{
"type": "plugin",
"id": "@kbn/observability-ai-assistant-management-plugin",
- "owner": "@elastic/obs-knowledge-team",
+ "owner": "@elastic/obs-ai-assistant",
"plugin": {
"id": "observabilityAiAssistantManagement",
"server": false,
From 3744a30b32a33215ec7e32c81a399e7b6e57569f Mon Sep 17 00:00:00 2001
From: Rickyanto Ang
Date: Wed, 10 Apr 2024 08:20:05 -0700
Subject: [PATCH 20/55] [Cloud Security][Bug] Fix for [Object object] issue on
secret fields (#179237)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
This PR addresses the issue where secret fields value are rendered as
[Object object] when user tries to edit it
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Maxim Kholod
---
.../aws_credentials_form.tsx | 1 +
.../aws_credentials_form_agentless.tsx | 8 +-
.../aws_input_var_fields.tsx | 86 ++++++--
.../get_aws_credentials_form_options.tsx | 18 +-
.../azure_credentials_form.tsx | 90 +++++---
.../azure_credentials_form_agentless.tsx | 2 +
.../get_azure_credentials_form_options.tsx | 6 +-
.../fleet_extensions/eks_credentials_form.tsx | 21 +-
.../components/fleet_extensions/mocks.ts | 98 ++++++++-
.../policy_template_form.test.tsx | 201 +++++++++++-------
.../components/fleet_extensions/utils.test.ts | 65 +++++-
.../components/fleet_extensions/utils.ts | 17 ++
.../public/components/test_subjects.ts | 2 +-
.../package_policy_input_var_field.tsx | 5 +-
x-pack/plugins/fleet/public/index.ts | 11 +-
15 files changed, 485 insertions(+), 146 deletions(-)
diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx
index 18d387d3136f3..009d7fc7a1473 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx
@@ -284,6 +284,7 @@ export const AwsCredentialsForm = ({
{
updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } }));
}}
diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx
index b0ab98db98cda..790bfdc606e82 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form_agentless.tsx
@@ -25,7 +25,12 @@ import {
AwsCredentialTypeSelector,
} from './aws_credentials_form';
-export const AwsCredentialsFormAgentless = ({ input, newPolicy, updatePolicy }: AwsFormProps) => {
+export const AwsCredentialsFormAgentless = ({
+ input,
+ newPolicy,
+ packageInfo,
+ updatePolicy,
+}: AwsFormProps) => {
const awsCredentialsType = getAwsCredentialsType(input) || DEFAULT_AGENTLESS_AWS_CREDENTIALS_TYPE;
const options = getAwsCredentialsFormOptions();
const group = options[awsCredentialsType];
@@ -74,6 +79,7 @@ export const AwsCredentialsFormAgentless = ({ input, newPolicy, updatePolicy }:
{
updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } }));
}}
diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_input_var_fields.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_input_var_fields.tsx
index 5ac54872a583b..960c1c3b39ddc 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_input_var_fields.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_input_var_fields.tsx
@@ -5,40 +5,80 @@
* 2.0.
*/
-import React from 'react';
-import { EuiFieldPassword, EuiFieldText, EuiFormRow } from '@elastic/eui';
+import React, { Suspense } from 'react';
+import { EuiFieldText, EuiFormRow, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui';
+import { PackageInfo } from '@kbn/fleet-plugin/common';
+import { css } from '@emotion/react';
+import { LazyPackagePolicyInputVarField } from '@kbn/fleet-plugin/public';
import { AwsOptions } from './get_aws_credentials_form_options';
+import { findVariableDef } from '../utils';
export const AwsInputVarFields = ({
fields,
onChange,
+ packageInfo,
}: {
fields: Array;
onChange: (key: string, value: string) => void;
-}) => (
-
- {fields.map((field) => (
-
+ packageInfo: PackageInfo;
+}) => {
+ return (
+
+ {fields.map((field) => (
<>
- {field.type === 'password' && (
-
onChange(field.id, event.target.value)}
- />
+ {field.type === 'password' && field.isSecret === true && (
+ <>
+
+
+ }>
+ {
+ onChange(field.id, value);
+ }}
+ errors={[]}
+ forceShowErrors={false}
+ isEditPage={true}
+ />
+
+
+
+ >
)}
{field.type === 'text' && (
- onChange(field.id, event.target.value)}
- />
+ hasChildLabel={true}
+ id={field.id}
+ >
+ onChange(field.id, event.target.value)}
+ />
+
)}
>
-
- ))}
-
-);
+ ))}
+
+ );
+};
diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/get_aws_credentials_form_options.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/get_aws_credentials_form_options.tsx
index 90175947ca172..d490e7cad5e27 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/get_aws_credentials_form_options.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/get_aws_credentials_form_options.tsx
@@ -71,7 +71,10 @@ const AWS_FIELD_LABEL = {
}),
};
-export type AwsCredentialsFields = Record;
+export type AwsCredentialsFields = Record<
+ string,
+ { label: string; type?: 'password' | 'text'; isSecret?: boolean }
+>;
export interface AwsOptionValue {
label: string;
@@ -89,6 +92,7 @@ export const getInputVarsFields = (input: NewPackagePolicyInput, fields: AwsCred
label: field.label,
type: field.type || 'text',
value: inputVar.value,
+ isSecret: field.isSecret,
} as const;
});
@@ -147,7 +151,11 @@ export const getAwsCredentialsFormOptions = (): AwsOptions => ({
info: DirectAccessKeysDescription,
fields: {
access_key_id: { label: AWS_FIELD_LABEL.access_key_id },
- secret_access_key: { label: AWS_FIELD_LABEL.secret_access_key, type: 'password' },
+ secret_access_key: {
+ label: AWS_FIELD_LABEL.secret_access_key,
+ type: 'password',
+ isSecret: true,
+ },
},
},
[AWS_CREDENTIALS_TYPE.TEMPORARY_KEYS]: {
@@ -157,7 +165,11 @@ export const getAwsCredentialsFormOptions = (): AwsOptions => ({
}),
fields: {
access_key_id: { label: AWS_FIELD_LABEL.access_key_id },
- secret_access_key: { label: AWS_FIELD_LABEL.secret_access_key, type: 'password' },
+ secret_access_key: {
+ label: AWS_FIELD_LABEL.secret_access_key,
+ type: 'password',
+ isSecret: true,
+ },
session_token: {
label: i18n.translate('xpack.csp.awsIntegration.sessionTokenLabel', {
defaultMessage: 'Session Token',
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 e1ae2ce16e242..18d76c9e7b90f 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
@@ -4,10 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import React, { useEffect } from 'react';
+import React, { Suspense, useEffect } from 'react';
import {
EuiCallOut,
- EuiFieldPassword,
EuiFieldText,
EuiFormRow,
EuiHorizontalRule,
@@ -16,6 +15,7 @@ import {
EuiSpacer,
EuiText,
EuiTitle,
+ EuiLoadingSpinner,
} from '@elastic/eui';
import type { NewPackagePolicy } from '@kbn/fleet-plugin/public';
import { NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common';
@@ -25,13 +25,14 @@ import { i18n } from '@kbn/i18n';
import semverValid from 'semver/functions/valid';
import semverCoerce from 'semver/functions/coerce';
import semverLt from 'semver/functions/lt';
+import { LazyPackagePolicyInputVarField } from '@kbn/fleet-plugin/public';
import {
AzureOptions,
getAzureCredentialsFormManualOptions,
} from './get_azure_credentials_form_options';
import { AzureCredentialsType } from '../../../../common/types_old';
import { useAzureCredentialsForm } from './hooks';
-import { getPosturePolicy, NewPackagePolicyPostureInput } from '../utils';
+import { findVariableDef, getPosturePolicy, NewPackagePolicyPostureInput } from '../utils';
import { CspRadioOption, RadioGroup } from '../csp_boxed_radio_group';
import { CIS_AZURE_SETUP_FORMAT_TEST_SUBJECTS } from '../../test_subjects';
@@ -267,39 +268,73 @@ const AZURE_MANUAL_FIELDS_PACKAGE_VERSION = '1.7.0';
export const AzureInputVarFields = ({
fields,
+ packageInfo,
onChange,
}: {
fields: Array;
+ packageInfo: PackageInfo;
onChange: (key: string, value: string) => void;
-}) => (
-
- {fields.map((field) => (
-
+}) => {
+ return (
+
+ {fields.map((field) => (
<>
- {field.type === 'password' && (
-
onChange(field.id, event.target.value)}
- data-test-subj={field.testSubj}
- />
+ {field.type === 'password' && field.isSecret === true && (
+ <>
+
+
+ }>
+ {
+ onChange(field.id, value);
+ }}
+ errors={[]}
+ forceShowErrors={false}
+ isEditPage={true}
+ />
+
+
+ >
)}
{field.type === 'text' && (
- onChange(field.id, event.target.value)}
- data-test-subj={field.testSubj}
- />
+ hasChildLabel={true}
+ id={field.id}
+ >
+ onChange(field.id, event.target.value)}
+ data-test-subj={field.testSubj}
+ />
+
)}
>
-
- ))}
-
-);
+ ))}
+
+ );
+};
export const AzureCredentialsForm = ({
input,
@@ -403,6 +438,7 @@ export const AzureCredentialsForm = ({
{
updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } }));
}}
@@ -412,7 +448,7 @@ export const AzureCredentialsForm = ({
{
const integrationLink = cspIntegrationDocsNavigation.cspm.getStartedPath;
const options = getAzureCredentialsFormOptions();
@@ -40,6 +41,7 @@ export const AzureCredentialsFormAgentless = ({
{
updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } }));
diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/get_azure_credentials_form_options.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/get_azure_credentials_form_options.tsx
index 97ef3ae78b974..0f46330a4f457 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/get_azure_credentials_form_options.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/azure_credentials_form/get_azure_credentials_form_options.tsx
@@ -16,7 +16,7 @@ import { AZURE_CREDENTIALS_TYPE } from './azure_credentials_form';
export type AzureCredentialsFields = Record<
string,
- { label: string; type?: 'password' | 'text'; testSubj?: string }
+ { label: string; type?: 'password' | 'text'; testSubj?: string; isSecret?: boolean }
>;
export interface AzureOptionValue {
@@ -55,6 +55,7 @@ export const getInputVarsFields = (input: NewPackagePolicyInput, fields: AzureCr
type: field.type || 'text',
testSubj: field.testSubj,
value: inputVar.value,
+ isSecret: field?.isSecret,
} as const;
});
@@ -107,6 +108,7 @@ export const getAzureCredentialsFormOptions = (): AzureOptions => ({
},
'azure.credentials.client_secret': {
type: 'password',
+ isSecret: true,
label: i18n.translate('xpack.csp.azureIntegration.clientSecretLabel', {
defaultMessage: 'Client Secret',
}),
@@ -135,6 +137,7 @@ export const getAzureCredentialsFormOptions = (): AzureOptions => ({
},
'azure.credentials.client_certificate_password': {
type: 'password',
+ isSecret: true,
label: i18n.translate('xpack.csp.azureIntegration.clientCertificatePasswordLabel', {
defaultMessage: 'Client Certificate Password',
}),
@@ -164,6 +167,7 @@ export const getAzureCredentialsFormOptions = (): AzureOptions => ({
},
'azure.credentials.client_password': {
type: 'password',
+ isSecret: true,
label: i18n.translate('xpack.csp.azureIntegration.clientPasswordLabel', {
defaultMessage: 'Client Password',
}),
diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_credentials_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_credentials_form.tsx
index fda6ae2f631fa..476aa627cfb53 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_credentials_form.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/eks_credentials_form.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { EuiLink, EuiSpacer, EuiText, EuiTitle, EuiHorizontalRule } from '@elastic/eui';
import type { NewPackagePolicy } from '@kbn/fleet-plugin/public';
-import { NewPackagePolicyInput } from '@kbn/fleet-plugin/common';
+import { NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { RadioGroup } from './csp_boxed_radio_group';
@@ -119,7 +119,7 @@ type AwsOptions = Record<
{
label: string;
info: React.ReactNode;
- fields: Record;
+ fields: Record;
testId: string;
}
>;
@@ -146,7 +146,11 @@ const options: AwsOptions = {
info: DirectAccessKeysDescription,
fields: {
access_key_id: { label: AWS_FIELD_LABEL.access_key_id },
- secret_access_key: { label: AWS_FIELD_LABEL.secret_access_key, type: 'password' },
+ secret_access_key: {
+ label: AWS_FIELD_LABEL.secret_access_key,
+ type: 'password',
+ isSecret: true,
+ },
},
testId: 'directAccessKeyTestId',
},
@@ -157,7 +161,11 @@ const options: AwsOptions = {
}),
fields: {
access_key_id: { label: AWS_FIELD_LABEL.access_key_id },
- secret_access_key: { label: AWS_FIELD_LABEL.secret_access_key, type: 'password' },
+ secret_access_key: {
+ label: AWS_FIELD_LABEL.secret_access_key,
+ type: 'password',
+ isSecret: true,
+ },
session_token: {
label: i18n.translate('xpack.csp.eksIntegration.sessionTokenLabel', {
defaultMessage: 'Session Token',
@@ -197,6 +205,7 @@ const AWS_CREDENTIALS_OPTIONS = Object.keys(options).map((value) => ({
interface Props {
newPolicy: NewPackagePolicy;
+ packageInfo: PackageInfo;
input: Extract;
updatePolicy(updatedPolicy: NewPackagePolicy): void;
}
@@ -214,13 +223,14 @@ const getInputVarsFields = (
label: field.label,
type: field.type || 'text',
value: inputVar.value,
+ isSecret: field?.isSecret,
} as const;
});
const getAwsCredentialsType = (input: Props['input']): AwsCredentialsType | undefined =>
input.streams[0].vars?.['aws.credentials.type'].value;
-export const EksCredentialsForm = ({ input, newPolicy, updatePolicy }: Props) => {
+export const EksCredentialsForm = ({ input, newPolicy, packageInfo, updatePolicy }: Props) => {
// We only have a value for 'aws.credentials.type' once the form has mounted.
// On initial render we don't have that value so we default to the first option.
const awsCredentialsType = getAwsCredentialsType(input) || AWS_CREDENTIALS_OPTIONS[0].id;
@@ -248,6 +258,7 @@ export const EksCredentialsForm = ({ input, newPolicy, updatePolicy }: Props) =>
updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } }))
}
diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts
index 887c8c471d9c8..38e6d5d632cea 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts
+++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/mocks.ts
@@ -7,6 +7,7 @@
import type { NewPackagePolicy } from '@kbn/fleet-plugin/public';
import type { PackageInfo, PackagePolicyConfigRecord } from '@kbn/fleet-plugin/common';
import { createNewPackagePolicyMock, createAgentPolicyMock } from '@kbn/fleet-plugin/common/mocks';
+import { RegistryRelease, RegistryVarType } from '@kbn/fleet-plugin/common/types';
import {
CLOUDBEAT_GCP,
CLOUDBEAT_AZURE,
@@ -31,6 +32,7 @@ export const getMockPolicyVulnMgmtAWS = () =>
export const getMockAgentlessAgentPolicy = () => {
return createAgentPolicyMock({ id: 'agentless' });
};
+export const getMockPackageInfo = () => getPackageInfoMock();
export const getMockPackageInfoVulnMgmtAWS = () => {
return {
@@ -142,7 +144,7 @@ const getPolicyMock = (
const awsVarsMock = {
access_key_id: { type: 'text' },
- secret_access_key: { type: 'text' },
+ secret_access_key: { type: 'password', isSecret: true },
session_token: { type: 'text' },
shared_credential_file: { type: 'text' },
credential_profile_name: { type: 'text' },
@@ -152,7 +154,7 @@ const getPolicyMock = (
const eksVarsMock = {
access_key_id: { type: 'text' },
- secret_access_key: { type: 'text' },
+ secret_access_key: { type: 'password', isSecret: true },
session_token: { type: 'text' },
shared_credential_file: { type: 'text' },
credential_profile_name: { type: 'text' },
@@ -262,3 +264,95 @@ const getPolicyMock = (
],
};
};
+
+export const getPackageInfoMock = () => {
+ return {
+ data_streams: [
+ {
+ dataset: 'cloud_security_posture.findings',
+ type: 'logs',
+
+ package: 'cloud_security_posture',
+ path: 'findings',
+ release: 'ga' as RegistryRelease,
+
+ title: 'Cloud Security Posture Findings',
+ streams: [
+ {
+ input: 'cloudbeat/cis_aws',
+ template_path: 'aws.yml.hbs',
+ title: 'CIS AWS Benchmark',
+ vars: [
+ {
+ name: 'secret_access_key',
+ title: 'Secret Access Key',
+ secret: true,
+ type: 'text' as RegistryVarType,
+ },
+ ],
+ },
+ {
+ input: 'cloudbeat/cis_eks',
+ template_path: 'eks.yml.hbs',
+ title: 'Amazon EKS Benchmark',
+ vars: [
+ {
+ name: 'secret_access_key',
+ title: 'Secret Access Key',
+ secret: true,
+ type: 'text' as RegistryVarType,
+ },
+ ],
+ },
+ {
+ input: 'cloudbeat/cis_azure',
+ template_path: 'azure.yml.hbs',
+ title: 'CIS Azure Benchmark',
+ vars: [
+ {
+ multi: false,
+ name: 'azure.credentials.client_secret',
+ required: false,
+ secret: true,
+ show_user: true,
+ title: 'Client Secret',
+ type: 'text' as RegistryVarType,
+ },
+ {
+ multi: false,
+ name: 'azure.credentials.client_password',
+ required: false,
+ secret: true,
+ show_user: true,
+ title: 'Client Password',
+ type: 'text' as RegistryVarType,
+ },
+ {
+ multi: false,
+ name: 'azure.credentials.client_certificate_password',
+ required: false,
+ secret: true,
+ show_user: true,
+ title: 'Client Certificate Password',
+ type: 'text' as RegistryVarType,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ format_version: '3.0.0',
+ version: '1.9.0-preview109',
+ name: 'cloud_security_posture',
+ description: 'Identify & remediate configuration risks in your Cloud infrastructure',
+ owner: {
+ github: 'elastic/cloud-security-posture',
+ type: 'elastic' as 'elastic' | 'partner' | 'community' | undefined,
+ },
+ title: 'Security Posture Management',
+ latestVersion: '1.9.0',
+ assets: {
+ kibana: {},
+ },
+ };
+};
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 ef680d95c7166..9704678e634f1 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
@@ -17,6 +17,7 @@ import {
import { TestProvider } from '../../test/test_provider';
import {
getMockAgentlessAgentPolicy,
+ getMockPackageInfo,
getMockPackageInfoCspmAWS,
getMockPackageInfoCspmAzure,
getMockPackageInfoCspmGCP,
@@ -27,6 +28,7 @@ import {
getMockPolicyGCP,
getMockPolicyK8s,
getMockPolicyVulnMgmtAWS,
+ getPackageInfoMock,
} from './mocks';
import type {
AgentPolicy,
@@ -58,6 +60,8 @@ import {
SETUP_TECHNOLOGY_SELECTOR_ACCORDION_TEST_SUBJ,
SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ,
} from '../test_subjects';
+import { ExperimentalFeaturesService } from '@kbn/fleet-plugin/public/services';
+import { ThemeProvider } from '@emotion/react';
// mock useParams
jest.mock('react-router-dom', () => ({
@@ -68,8 +72,10 @@ jest.mock('react-router-dom', () => ({
}));
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/api/use_package_policy_list');
+jest.mock('@kbn/fleet-plugin/public/services/experimental_features');
const onChange = jest.fn();
+const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService);
const createReactQueryResponseWithRefetch = (
data: Parameters[0]
@@ -85,6 +91,9 @@ describe(' ', () => {
(useParams as jest.Mock).mockReturnValue({
integration: undefined,
});
+ mockedExperimentalFeaturesService.get.mockReturnValue({
+ secretsStorage: true,
+ } as any);
(usePackagePolicyList as jest.Mock).mockImplementation((packageName) =>
createReactQueryResponseWithRefetch({
status: 'success',
@@ -116,29 +125,31 @@ describe(' ', () => {
onChange?: jest.Mock;
agentlessPolicy?: AgentPolicy;
}) => (
-
- {edit && (
-
- )}
- {!edit && (
-
- )}
-
+ ({ eui: { euiSizeXS: '4px' } })}>
+
+ {edit && (
+
+ )}
+ {!edit && (
+
+ )}
+
+
);
it('updates package policy namespace to default when it changes', () => {
@@ -625,29 +636,32 @@ describe(' ', () => {
});
});
- it(`renders ${CLOUDBEAT_EKS} Direct Access Keys fields`, () => {
- let policy: NewPackagePolicy = getMockPolicyEKS();
+ it(`renders ${CLOUDBEAT_EKS} Direct Access Keys fields`, async () => {
+ let policy = getMockPolicyEKS();
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
'aws.credentials.type': { value: 'direct_access_keys' },
'aws.setup.format': { value: 'manual' },
});
-
- const { getByLabelText } = render( );
+ const { getByLabelText } = render(
+
+ );
const option = getByLabelText('Direct access keys');
expect(option).toBeChecked();
expect(getByLabelText('Access Key ID')).toBeInTheDocument();
- expect(getByLabelText('Secret Access Key')).toBeInTheDocument();
+ await waitFor(() => expect(getByLabelText('Secret Access Key')).toBeInTheDocument());
});
- it(`updates ${CLOUDBEAT_EKS} Direct Access Keys fields`, () => {
+ it(`updates ${CLOUDBEAT_EKS} Direct Access Keys fields`, async () => {
let policy = getMockPolicyEKS();
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
'aws.credentials.type': { value: 'direct_access_keys' },
'aws.setup.format': { value: 'manual' },
});
- const { getByLabelText, rerender } = render( );
+ const { getByLabelText, rerender, getByTestId } = render(
+
+ );
userEvent.type(getByLabelText('Access Key ID'), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { access_key_id: { value: 'a' } });
@@ -658,10 +672,12 @@ describe(' ', () => {
updatedPolicy: policy,
});
- rerender( );
+ rerender(
+
+ );
- userEvent.type(getByLabelText('Secret Access Key'), 'b');
- policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { secret_access_key: { value: 'b' } });
+ await waitFor(() => userEvent.type(getByTestId('passwordInput-secret-access-key'), 'c'));
+ policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { secret_access_key: { value: 'c' } });
expect(onChange).toHaveBeenCalledWith({
isValid: true,
@@ -669,30 +685,34 @@ describe(' ', () => {
});
});
- it(`renders ${CLOUDBEAT_EKS} Temporary Keys fields`, () => {
+ it(`renders ${CLOUDBEAT_EKS} Temporary Keys fields`, async () => {
let policy: NewPackagePolicy = getMockPolicyEKS();
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
'aws.credentials.type': { value: 'temporary_keys' },
'aws.setup.format': { value: 'manual' },
});
- const { getByLabelText } = render( );
+ const { getByLabelText } = render(
+
+ );
const option = getByLabelText('Temporary keys');
expect(option).toBeChecked();
expect(getByLabelText('Access Key ID')).toBeInTheDocument();
- expect(getByLabelText('Secret Access Key')).toBeInTheDocument();
+ await waitFor(() => expect(getByLabelText('Secret Access Key')).toBeInTheDocument());
expect(getByLabelText('Session Token')).toBeInTheDocument();
});
- it(`updates ${CLOUDBEAT_EKS} Temporary Keys fields`, () => {
+ it(`updates ${CLOUDBEAT_EKS} Temporary Keys fields`, async () => {
let policy = getMockPolicyEKS();
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, {
'aws.credentials.type': { value: 'temporary_keys' },
'aws.setup.format': { value: 'manual' },
});
- const { getByLabelText, rerender } = render( );
+ const { getByLabelText, rerender, getByTestId } = render(
+
+ );
userEvent.type(getByLabelText('Access Key ID'), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { access_key_id: { value: 'a' } });
@@ -702,17 +722,21 @@ describe(' ', () => {
updatedPolicy: policy,
});
- rerender( );
+ rerender(
+
+ );
- userEvent.type(getByLabelText('Secret Access Key'), 'b');
- policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { secret_access_key: { value: 'b' } });
+ await waitFor(() => userEvent.type(getByTestId('passwordInput-secret-access-key'), 'c'));
+ policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { secret_access_key: { value: 'c' } });
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: policy,
});
- rerender( );
+ rerender(
+
+ );
userEvent.type(getByLabelText('Session Token'), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_EKS, { session_token: { value: 'a' } });
@@ -907,30 +931,34 @@ describe(' ', () => {
});
});
- it(`renders ${CLOUDBEAT_AWS} Direct Access Keys fields`, () => {
+ it(`renders ${CLOUDBEAT_AWS} Direct Access Keys fields`, async () => {
let policy: NewPackagePolicy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'direct_access_keys' },
'aws.setup.format': { value: 'manual' },
});
- const { getByLabelText, getByRole } = render( );
+ const { getByLabelText, getByRole } = render(
+
+ );
expect(
getByRole('option', { name: 'Direct access keys', selected: true })
).toBeInTheDocument();
expect(getByLabelText('Access Key ID')).toBeInTheDocument();
- expect(getByLabelText('Secret Access Key')).toBeInTheDocument();
+ await waitFor(() => expect(getByLabelText('Secret Access Key')).toBeInTheDocument());
});
- it(`updates ${CLOUDBEAT_AWS} Direct Access Keys fields`, () => {
+ it(`updates ${CLOUDBEAT_AWS} Direct Access Keys fields`, async () => {
let policy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'direct_access_keys' },
'aws.setup.format': { value: 'manual' },
});
- const { getByLabelText, rerender } = render( );
+ const { getByLabelText, rerender, getByTestId } = render(
+
+ );
userEvent.type(getByLabelText('Access Key ID'), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { access_key_id: { value: 'a' } });
@@ -941,9 +969,11 @@ describe(' ', () => {
updatedPolicy: policy,
});
- rerender( );
+ rerender(
+
+ );
- userEvent.type(getByLabelText('Secret Access Key'), 'b');
+ await waitFor(() => userEvent.type(getByTestId('passwordInput-secret-access-key'), 'b'));
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { secret_access_key: { value: 'b' } });
expect(onChange).toHaveBeenCalledWith({
@@ -952,28 +982,32 @@ describe(' ', () => {
});
});
- it(`renders ${CLOUDBEAT_AWS} Temporary Keys fields`, () => {
+ it(`renders ${CLOUDBEAT_AWS} Temporary Keys fields`, async () => {
let policy: NewPackagePolicy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'temporary_keys' },
'aws.setup.format': { value: 'manual' },
});
- const { getByLabelText, getByRole } = render( );
+ const { getByLabelText, getByRole } = render(
+
+ );
expect(getByRole('option', { name: 'Temporary keys', selected: true })).toBeInTheDocument();
expect(getByLabelText('Access Key ID')).toBeInTheDocument();
- expect(getByLabelText('Secret Access Key')).toBeInTheDocument();
+ await waitFor(() => expect(getByLabelText('Secret Access Key')).toBeInTheDocument());
expect(getByLabelText('Session Token')).toBeInTheDocument();
});
- it(`updates ${CLOUDBEAT_AWS} Temporary Keys fields`, () => {
+ it(`updates ${CLOUDBEAT_AWS} Temporary Keys fields`, async () => {
let policy = getMockPolicyAWS();
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, {
'aws.credentials.type': { value: 'temporary_keys' },
'aws.setup.format': { value: 'manual' },
});
- const { getByLabelText, rerender } = render( );
+ const { getByLabelText, rerender, getByTestId } = render(
+
+ );
userEvent.type(getByLabelText('Access Key ID'), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { access_key_id: { value: 'a' } });
@@ -983,9 +1017,16 @@ describe(' ', () => {
updatedPolicy: policy,
});
- rerender( );
+ expect(onChange).toHaveBeenCalledWith({
+ isValid: true,
+ updatedPolicy: policy,
+ });
+
+ rerender(
+
+ );
- userEvent.type(getByLabelText('Secret Access Key'), 'b');
+ await waitFor(() => userEvent.type(getByTestId('passwordInput-secret-access-key'), 'b'));
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { secret_access_key: { value: 'b' } });
expect(onChange).toHaveBeenCalledWith({
@@ -993,7 +1034,9 @@ describe(' ', () => {
updatedPolicy: policy,
});
- rerender( );
+ rerender(
+
+ );
userEvent.type(getByLabelText('Session Token'), 'a');
policy = getPosturePolicy(policy, CLOUDBEAT_AWS, { session_token: { value: 'a' } });
@@ -1384,14 +1427,14 @@ describe(' ', () => {
});
});
- it(`renders ${CLOUDBEAT_AZURE} Service Principal with Client Secret fields`, () => {
+ it(`renders ${CLOUDBEAT_AZURE} Service Principal with Client Secret fields`, async () => {
let policy = getMockPolicyAzure();
policy = getPosturePolicy(policy, CLOUDBEAT_AZURE, {
'azure.credentials.type': { value: 'service_principal_with_client_secret' },
});
const { getByLabelText, getByRole } = render(
-
+
);
expect(
@@ -1399,17 +1442,17 @@ describe(' ', () => {
).toBeInTheDocument();
expect(getByLabelText('Tenant ID')).toBeInTheDocument();
expect(getByLabelText('Client ID')).toBeInTheDocument();
- expect(getByLabelText('Client Secret')).toBeInTheDocument();
+ await waitFor(() => expect(getByLabelText('Client Secret')).toBeInTheDocument());
});
- it(`updates ${CLOUDBEAT_AZURE} Service Principal with Client Secret fields`, () => {
+ it(`updates ${CLOUDBEAT_AZURE} Service Principal with Client Secret fields`, async () => {
let policy = getMockPolicyAzure();
policy = getPosturePolicy(policy, CLOUDBEAT_AZURE, {
'azure.credentials.type': { value: 'service_principal_with_client_secret' },
});
- const { rerender, getByLabelText } = render(
-
+ const { rerender, getByLabelText, getByTestId } = render(
+
);
userEvent.type(getByLabelText('Tenant ID'), 'a');
@@ -1424,7 +1467,7 @@ describe(' ', () => {
});
rerender(
-
+
);
userEvent.type(getByLabelText('Client ID'), 'b');
@@ -1438,10 +1481,10 @@ describe(' ', () => {
});
rerender(
-
+
);
- userEvent.type(getByLabelText('Client Secret'), 'c');
+ await waitFor(() => userEvent.type(getByTestId('passwordInput-client-secret'), 'c'));
policy = getPosturePolicy(policy, CLOUDBEAT_AZURE, {
'azure.credentials.client_secret': { value: 'c' },
});
@@ -1600,7 +1643,7 @@ describe(' ', () => {
);
@@ -1649,7 +1692,7 @@ describe(' ', () => {
);
@@ -1709,14 +1752,14 @@ describe(' ', () => {
});
});
- it(`renders Service principal with Client Certificate fields`, () => {
+ it(`renders Service principal with Client Certificate fields`, async () => {
let policy = getMockPolicyAzure();
policy = getPosturePolicy(policy, CLOUDBEAT_AZURE, {
'azure.credentials.type': { value: 'service_principal_with_client_certificate' },
});
const { getByLabelText, getByRole } = render(
-
+
);
expect(
@@ -1725,17 +1768,17 @@ describe(' ', () => {
expect(getByLabelText('Tenant ID')).toBeInTheDocument();
expect(getByLabelText('Client ID')).toBeInTheDocument();
expect(getByLabelText('Client Certificate Path')).toBeInTheDocument();
- expect(getByLabelText('Client Certificate Password')).toBeInTheDocument();
+ await waitFor(() => expect(getByLabelText('Client Certificate Password')).toBeInTheDocument());
});
- it(`updates Service principal with Client Certificate fields`, () => {
+ it(`updates Service principal with Client Certificate fields`, async () => {
let policy = getMockPolicyAzure();
policy = getPosturePolicy(policy, CLOUDBEAT_AZURE, {
'azure.credentials.type': { value: 'service_principal_with_client_certificate' },
});
- const { rerender, getByLabelText } = render(
-
+ const { rerender, getByLabelText, getByTestId } = render(
+
);
userEvent.type(getByLabelText('Tenant ID'), 'a');
@@ -1750,7 +1793,7 @@ describe(' ', () => {
});
rerender(
-
+
);
userEvent.type(getByLabelText('Client ID'), 'b');
@@ -1764,7 +1807,7 @@ describe(' ', () => {
});
rerender(
-
+
);
userEvent.type(getByLabelText('Client Certificate Path'), 'c');
@@ -1778,10 +1821,12 @@ describe(' ', () => {
});
rerender(
-
+
);
- userEvent.type(getByLabelText('Client Certificate Password'), 'd');
+ await waitFor(() =>
+ userEvent.type(getByTestId('passwordInput-client-certificate-password'), 'd')
+ );
policy = getPosturePolicy(policy, CLOUDBEAT_AZURE, {
'azure.credentials.client_certificate_password': { value: 'd' },
});
diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts
index 9475298b2d6f3..13bb5398de44c 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts
+++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts
@@ -16,8 +16,9 @@ import {
getDefaultAwsCredentialsType,
getDefaultAzureCredentialsType,
getDefaultGcpHiddenVars,
+ findVariableDef,
} from './utils';
-import { getMockPolicyAWS, getMockPolicyK8s, getMockPolicyEKS } from './mocks';
+import { getMockPolicyAWS, getMockPolicyK8s, getMockPolicyEKS, getPackageInfoMock } from './mocks';
describe('getPosturePolicy', () => {
for (const [name, getPolicy, expectedVars] of [
@@ -486,3 +487,65 @@ describe('getDefaultGcpHiddenVars', () => {
});
});
});
+
+describe('findVariableDef', () => {
+ it('Should return var item when key exist', () => {
+ const packageInfo = getPackageInfoMock() as PackageInfo;
+ const key = 'secret_access_key';
+ const result = findVariableDef(packageInfo, key);
+
+ expect(result).toMatchObject({
+ name: 'secret_access_key',
+ secret: true,
+ title: 'Secret Access Key',
+ });
+ });
+
+ it('Should return undefined when key is invalid', () => {
+ const packageInfo = getPackageInfoMock() as PackageInfo;
+ const key = 'invalid_access_key';
+ const result = findVariableDef(packageInfo, key);
+
+ expect(result).toBeUndefined();
+ });
+
+ it('Should return undefined when datastream is undefined', () => {
+ const packageInfo = {
+ data_streams: [{}],
+ } as PackageInfo;
+ const key = 'secret_access_key';
+ const result = findVariableDef(packageInfo, key);
+
+ expect(result).toBeUndefined();
+ });
+
+ it('Should return undefined when stream is undefined', () => {
+ const packageInfo = {
+ data_streams: [
+ {
+ title: 'Cloud Security Posture Findings',
+ streams: [{}],
+ },
+ ],
+ } as PackageInfo;
+ const key = 'secret_access_key';
+ const result = findVariableDef(packageInfo, key);
+
+ expect(result).toBeUndefined();
+ });
+
+ it('Should return undefined when stream.var is invalid', () => {
+ const packageInfo = {
+ data_streams: [
+ {
+ title: 'Cloud Security Posture Findings',
+ streams: [{ vars: {} }],
+ },
+ ],
+ } as PackageInfo;
+ const key = 'secret_access_key';
+ const result = findVariableDef(packageInfo, key);
+
+ expect(result).toBeUndefined();
+ });
+});
diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts
index 1a42e5dd5c54f..8090117ad5df2 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts
+++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts
@@ -378,3 +378,20 @@ export const isBelowMinVersion = (version: string, minVersion: string) => {
const versionNumberOnly = semverCoerce(semanticVersion) || '';
return semverLt(versionNumberOnly, minVersion);
};
+
+/**
+ * Searches for a variable definition in a given packageInfo object based on a specified key.
+ * It navigates through nested arrays within the packageInfo object to locate the variable definition associated with the provided key.
+ * If found, it returns the variable definition object; otherwise, it returns undefined.
+ */
+export const findVariableDef = (packageInfo: PackageInfo, key: string) => {
+ return packageInfo?.data_streams
+ ?.filter((datastreams) => datastreams !== undefined)
+ .map((ds) => ds.streams)
+ .filter((streams) => streams !== undefined)
+ .flat()
+ .filter((streams) => streams?.vars !== undefined)
+ .map((cis) => cis?.vars)
+ .flat()
+ .find((vars) => vars?.name === key);
+};
diff --git a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts
index ca6fc35c75340..0872dff084498 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts
+++ b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts
@@ -66,7 +66,7 @@ export const SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ = 'setup-technology-selector';
export const CIS_AZURE_INPUT_FIELDS_TEST_SUBJECTS = {
TENANT_ID: 'cisAzureTenantId',
CLIENT_ID: 'cisAzureClientId',
- CLIENT_SECRET: 'cisAzureClientSecret',
+ CLIENT_SECRET: 'passwordInput-client-secret',
CLIENT_CERTIFICATE_PATH: 'cisAzureClientCertificatePath',
CLIENT_CERTIFICATE_PASSWORD: 'cisAzureClientCertificatePassword',
CLIENT_USERNAME: 'cisAzureClientUsername',
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx
index eb4e6dd5f5c69..bc326f92edb68 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field.tsx
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import React, { useState, memo, useMemo, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import { i18n } from '@kbn/i18n';
@@ -51,11 +50,11 @@ const FormRow = styled(EuiFormRow)`
}
.euiFormRow__fieldWrapper > .euiPanel {
- padding: ${(props) => props.theme.eui.euiSizeXS};
+ padding: ${(props) => props.theme.eui?.euiSizeXS};
}
`;
-interface InputFieldProps {
+export interface InputFieldProps {
varDef: RegistryVarsEntry;
value: any;
onChange: (newValue: any) => void;
diff --git a/x-pack/plugins/fleet/public/index.ts b/x-pack/plugins/fleet/public/index.ts
index 685a50230912b..7661cbc64ad31 100644
--- a/x-pack/plugins/fleet/public/index.ts
+++ b/x-pack/plugins/fleet/public/index.ts
@@ -7,6 +7,8 @@
import type { PluginInitializerContext } from '@kbn/core/public';
+import { lazy } from 'react';
+
import { FleetPlugin } from './plugin';
export type { FleetSetup, FleetStart, FleetStartServices } from './plugin';
@@ -62,9 +64,16 @@ export { PackagePolicyEditorDatastreamPipelines } from './applications/fleet/sec
export type { PackagePolicyEditorDatastreamPipelinesProps } from './applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_pipelines';
export { PackagePolicyEditorDatastreamMappings } from './applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_mappings';
export type { PackagePolicyEditorDatastreamMappingsProps } from './applications/fleet/sections/agent_policy/create_package_policy_page/components/datastream_mappings';
-
export type { DynamicPagePathValues } from './constants';
+// This Type export is added to prevent error TS4023
+export type { InputFieldProps } from './applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field';
+
+export const LazyPackagePolicyInputVarField = lazy(() =>
+ import(
+ './applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_var_field'
+ ).then((module) => ({ default: module.PackagePolicyInputVarField }))
+);
export type { PackageListGridProps } from './applications/integrations/sections/epm/components/package_list_grid';
export type { AvailablePackagesHookType } from './applications/integrations/sections/epm/screens/home/hooks/use_available_packages';
export type { IntegrationCardItem } from './applications/integrations/sections/epm/screens/home';
From 1fa7a69fb6b0e54b34af30531c8df28524dcff55 Mon Sep 17 00:00:00 2001
From: Sander Philipse <94373878+sphilipse@users.noreply.github.com>
Date: Wed, 10 Apr 2024 17:48:06 +0200
Subject: [PATCH 21/55] Move shared index management types to package (#179932)
## Summary
This moves a number of types that are used outside of the index
management plugin to a package so we can avoid cyclical dependencies in
the work we're doing to add semantic text as a mapping type. That will
depend on the ML plugin, which has dependencies that themselves depend
on a few types from index management. I split this into a separate PR
for ease of reviewing.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli
---
.github/CODEOWNERS | 1 +
package.json | 1 +
.../src/text_based_languages_editor.tsx | 2 +-
packages/kbn-text-based-editor/tsconfig.json | 2 +-
.../public/kibana_services.ts | 2 +-
.../text_based_languages/public/plugin.ts | 2 +-
.../text_based_languages/tsconfig.json | 3 +-
tsconfig.base.json | 6 +-
x-pack/.i18nrc.json | 1 +
x-pack/packages/index-management/README.md | 3 +
x-pack/packages/index-management/index.ts | 10 +++
.../packages/index-management/jest.config.js | 12 +++
x-pack/packages/index-management/kibana.jsonc | 5 ++
x-pack/packages/index-management/package.json | 6 ++
.../index-management/src}/home_sections.ts | 2 +-
.../src/services/extensions_service.ts | 71 ++++++++++++++++
.../index-management/src/services/index.ts | 8 ++
.../src/services/public_api_service.ts | 22 +++++
x-pack/packages/index-management/src/types.ts | 85 +++++++++++++++++++
.../packages/index-management/tsconfig.json | 26 ++++++
.../components/create/description.test.tsx | 2 +-
x-pack/plugins/cases/tsconfig.json | 3 -
.../shared/kibana/kibana_logic.ts | 2 +-
.../enterprise_search/public/plugin.ts | 2 +-
.../plugins/enterprise_search/tsconfig.json | 2 +-
.../components/index_lifecycle_summary.tsx | 3 +-
.../index_lifecycle_management/tsconfig.json | 3 +-
.../common/constants/index.ts | 4 +-
.../common/lib/enrich_policies.ts | 2 +-
.../common/types/enrich_policies.ts | 11 ---
.../index_management/common/types/indices.ts | 32 +------
.../components/no_match/no_match.tsx | 2 +-
.../application/mount_management_section.ts | 4 +-
.../create_policy_context.tsx | 2 +-
.../create_policy_wizard.tsx | 2 +-
.../enrich_policy_create/steps/create.tsx | 2 +-
.../details_flyout/policy_details_flyout.tsx | 2 +-
.../enrich_policies_list.tsx | 2 +-
.../policies_table/policies_table.tsx | 2 +-
.../index_mapping_with_context_types.tsx | 30 +------
.../public/application/services/api.ts | 3 +-
.../plugins/index_management/public/index.ts | 11 +--
.../plugins/index_management/public/plugin.ts | 12 +--
.../services/extensions_service.mock.ts | 3 +-
.../public/services/extensions_service.ts | 73 ++--------------
.../index_management/public/services/index.ts | 2 -
.../services/public_api_service.mock.ts | 2 +-
.../public/services/public_api_service.ts | 11 +--
.../plugins/index_management/public/types.ts | 38 ---------
.../server/lib/enrich_policies.ts | 2 +-
.../enrich_policies/register_create_route.ts | 2 +-
x-pack/plugins/index_management/tsconfig.json | 10 ++-
.../index_mappings_docs_link.tsx | 2 +-
.../index_overview_content.tsx | 2 +-
.../plugins/serverless_search/tsconfig.json | 1 +
yarn.lock | 4 +
56 files changed, 328 insertions(+), 234 deletions(-)
create mode 100644 x-pack/packages/index-management/README.md
create mode 100644 x-pack/packages/index-management/index.ts
create mode 100644 x-pack/packages/index-management/jest.config.js
create mode 100644 x-pack/packages/index-management/kibana.jsonc
create mode 100644 x-pack/packages/index-management/package.json
rename x-pack/{plugins/index_management/common/constants => packages/index-management/src}/home_sections.ts (97%)
create mode 100644 x-pack/packages/index-management/src/services/extensions_service.ts
create mode 100644 x-pack/packages/index-management/src/services/index.ts
create mode 100644 x-pack/packages/index-management/src/services/public_api_service.ts
create mode 100644 x-pack/packages/index-management/src/types.ts
create mode 100644 x-pack/packages/index-management/tsconfig.json
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 5cc46aad384a3..70640a4502c6f 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -478,6 +478,7 @@ src/plugins/image_embeddable @elastic/appex-sharedux
packages/kbn-import-locator @elastic/kibana-operations
packages/kbn-import-resolver @elastic/kibana-operations
x-pack/plugins/index_lifecycle_management @elastic/kibana-management
+x-pack/packages/index-management @elastic/kibana-management
x-pack/plugins/index_management @elastic/kibana-management
test/plugin_functional/plugins/index_patterns @elastic/kibana-data-discovery
x-pack/packages/kbn-infra-forge @elastic/obs-ux-management-team
diff --git a/package.json b/package.json
index dfb2282bf15c1..9546120c02433 100644
--- a/package.json
+++ b/package.json
@@ -509,6 +509,7 @@
"@kbn/iframe-embedded-plugin": "link:x-pack/test/functional_embedded/plugins/iframe_embedded",
"@kbn/image-embeddable-plugin": "link:src/plugins/image_embeddable",
"@kbn/index-lifecycle-management-plugin": "link:x-pack/plugins/index_lifecycle_management",
+ "@kbn/index-management": "link:x-pack/packages/index-management",
"@kbn/index-management-plugin": "link:x-pack/plugins/index_management",
"@kbn/index-patterns-test-plugin": "link:test/plugin_functional/plugins/index_patterns",
"@kbn/infra-forge": "link:x-pack/packages/kbn-infra-forge",
diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
index 5c83b1bffb540..4c4440fe70029 100644
--- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
+++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx
@@ -22,7 +22,7 @@ import { getAggregateQueryMode, getLanguageDisplayName } from '@kbn/es-query';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import type { CoreStart } from '@kbn/core/public';
-import type { IndexManagementPluginSetup } from '@kbn/index-management-plugin/public';
+import type { IndexManagementPluginSetup } from '@kbn/index-management';
import { TooltipWrapper } from '@kbn/visualization-utils';
import {
type LanguageDocumentationSections,
diff --git a/packages/kbn-text-based-editor/tsconfig.json b/packages/kbn-text-based-editor/tsconfig.json
index ffd9ff79b6aad..78cee94bd68c8 100644
--- a/packages/kbn-text-based-editor/tsconfig.json
+++ b/packages/kbn-text-based-editor/tsconfig.json
@@ -23,7 +23,7 @@
"@kbn/data-plugin",
"@kbn/expressions-plugin",
"@kbn/data-views-plugin",
- "@kbn/index-management-plugin",
+ "@kbn/index-management",
"@kbn/visualization-utils",
"@kbn/code-editor",
"@kbn/shared-ux-markdown",
diff --git a/src/plugins/text_based_languages/public/kibana_services.ts b/src/plugins/text_based_languages/public/kibana_services.ts
index 8592904a69370..d9e39e3aefa68 100644
--- a/src/plugins/text_based_languages/public/kibana_services.ts
+++ b/src/plugins/text_based_languages/public/kibana_services.ts
@@ -10,7 +10,7 @@ import { BehaviorSubject } from 'rxjs';
import type { CoreStart } from '@kbn/core/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
-import { IndexManagementPluginSetup } from '@kbn/index-management-plugin/public';
+import { IndexManagementPluginSetup } from '@kbn/index-management';
export let core: CoreStart;
diff --git a/src/plugins/text_based_languages/public/plugin.ts b/src/plugins/text_based_languages/public/plugin.ts
index d496bdfe30f99..dd6297750ae5b 100755
--- a/src/plugins/text_based_languages/public/plugin.ts
+++ b/src/plugins/text_based_languages/public/plugin.ts
@@ -9,7 +9,7 @@
import type { Plugin, CoreStart, CoreSetup } from '@kbn/core/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
-import type { IndexManagementPluginSetup } from '@kbn/index-management-plugin/public';
+import type { IndexManagementPluginSetup } from '@kbn/index-management';
import { setKibanaServices } from './kibana_services';
interface TextBasedLanguagesPluginStart {
diff --git a/src/plugins/text_based_languages/tsconfig.json b/src/plugins/text_based_languages/tsconfig.json
index 152a2aba25c6b..8d3fef08e23a9 100644
--- a/src/plugins/text_based_languages/tsconfig.json
+++ b/src/plugins/text_based_languages/tsconfig.json
@@ -15,10 +15,9 @@
"@kbn/core",
"@kbn/expressions-plugin",
"@kbn/data-views-plugin",
- "@kbn/index-management-plugin",
+ "@kbn/index-management"
],
"exclude": [
"target/**/*",
]
}
-
diff --git a/tsconfig.base.json b/tsconfig.base.json
index fef6173947031..45b0f4e18beaa 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -950,6 +950,8 @@
"@kbn/import-resolver/*": ["packages/kbn-import-resolver/*"],
"@kbn/index-lifecycle-management-plugin": ["x-pack/plugins/index_lifecycle_management"],
"@kbn/index-lifecycle-management-plugin/*": ["x-pack/plugins/index_lifecycle_management/*"],
+ "@kbn/index-management": ["x-pack/packages/index-management"],
+ "@kbn/index-management/*": ["x-pack/packages/index-management/*"],
"@kbn/index-management-plugin": ["x-pack/plugins/index_management"],
"@kbn/index-management-plugin/*": ["x-pack/plugins/index_management/*"],
"@kbn/index-patterns-test-plugin": ["test/plugin_functional/plugins/index_patterns"],
@@ -1814,7 +1816,9 @@
"@kbn/zod-helpers/*": ["packages/kbn-zod-helpers/*"],
// END AUTOMATED PACKAGE LISTING
// Allows for importing from `kibana` package for the exported types.
- "@emotion/core": ["typings/@emotion"]
+ "@emotion/core": [
+ "typings/@emotion"
+ ]
},
// Support .tsx files and transform JSX into calls to React.createElement
"jsx": "react",
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index 98a4912804d1e..2e1f2d5855ddc 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -48,6 +48,7 @@
],
"xpack.grokDebugger": "plugins/grokdebugger",
"xpack.idxMgmt": "plugins/index_management",
+ "xpack.idxMgmtPackage": "packages/index-management",
"xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management",
"xpack.infra": "plugins/observability_solution/infra",
"xpack.logsExplorer": "plugins/observability_solution/logs_explorer",
diff --git a/x-pack/packages/index-management/README.md b/x-pack/packages/index-management/README.md
new file mode 100644
index 0000000000000..438a6b4393892
--- /dev/null
+++ b/x-pack/packages/index-management/README.md
@@ -0,0 +1,3 @@
+# @kbn/index-management
+
+Contains types and functions used and exported by the index management plugin. Primarily used to avoid cyclical dependencies.
diff --git a/x-pack/packages/index-management/index.ts b/x-pack/packages/index-management/index.ts
new file mode 100644
index 0000000000000..d89dd466bd272
--- /dev/null
+++ b/x-pack/packages/index-management/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './src/types';
+export * from './src/home_sections';
+export * from './src/services';
diff --git a/x-pack/packages/index-management/jest.config.js b/x-pack/packages/index-management/jest.config.js
new file mode 100644
index 0000000000000..0d2b7feaaf550
--- /dev/null
+++ b/x-pack/packages/index-management/jest.config.js
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../..',
+ roots: ['/x-pack/packages/index-management'],
+};
diff --git a/x-pack/packages/index-management/kibana.jsonc b/x-pack/packages/index-management/kibana.jsonc
new file mode 100644
index 0000000000000..dc4e61342c6bc
--- /dev/null
+++ b/x-pack/packages/index-management/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-common",
+ "id": "@kbn/index-management",
+ "owner": "@elastic/kibana-management"
+}
diff --git a/x-pack/packages/index-management/package.json b/x-pack/packages/index-management/package.json
new file mode 100644
index 0000000000000..c1f7664895348
--- /dev/null
+++ b/x-pack/packages/index-management/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/index-management",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0"
+}
diff --git a/x-pack/plugins/index_management/common/constants/home_sections.ts b/x-pack/packages/index-management/src/home_sections.ts
similarity index 97%
rename from x-pack/plugins/index_management/common/constants/home_sections.ts
rename to x-pack/packages/index-management/src/home_sections.ts
index d44e64acbbad8..aa69f3c034303 100644
--- a/x-pack/plugins/index_management/common/constants/home_sections.ts
+++ b/x-pack/packages/index-management/src/home_sections.ts
@@ -8,7 +8,7 @@
import { FunctionComponent, ReactNode } from 'react';
import { ApplicationStart } from '@kbn/core-application-browser';
import { EuiBreadcrumb } from '@elastic/eui';
-import { Index } from '../types';
+import { Index } from './types';
export enum Section {
Indices = 'indices',
diff --git a/x-pack/packages/index-management/src/services/extensions_service.ts b/x-pack/packages/index-management/src/services/extensions_service.ts
new file mode 100644
index 0000000000000..047c290ba06ec
--- /dev/null
+++ b/x-pack/packages/index-management/src/services/extensions_service.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 { FunctionComponent, ReactNode } from 'react';
+import { ApplicationStart } from '@kbn/core-application-browser';
+import { EuiBadgeProps } from '@elastic/eui';
+import { IndexDetailsTab } from '../home_sections';
+import { Index } from '../types';
+
+export interface IndexContent {
+ renderContent: (args: {
+ index: Index;
+ getUrlForApp: ApplicationStart['getUrlForApp'];
+ }) => ReturnType;
+}
+
+export interface IndexToggle {
+ matchIndex: (index: Index) => boolean;
+ label: string;
+ name: string;
+}
+export interface IndexBadge {
+ matchIndex: (index: Index) => boolean;
+ label: string;
+ // a parseable search bar filter expression, for example "isFollowerIndex:true"
+ filterExpression?: string;
+ color: EuiBadgeProps['color'];
+}
+
+export interface EmptyListContent {
+ renderContent: (args: {
+ // the button to open the "create index" modal
+ createIndexButton: ReturnType;
+ }) => ReturnType;
+}
+
+export interface IndicesListColumn {
+ fieldName: string;
+ label: string;
+ order: number;
+ render?: (index: Index) => ReactNode;
+ // return a value used for sorting (only if the value is different from the original value at index[fieldName])
+ sort?: (index: Index) => any;
+}
+
+export interface ExtensionsSetup {
+ // adds an option to the "manage index" menu
+ addAction(action: any): void;
+ // adds a banner to the indices list
+ addBanner(banner: any): void;
+ // adds a filter to the indices list
+ addFilter(filter: any): void;
+ // adds a badge to the index name
+ addBadge(badge: IndexBadge): void;
+ // adds a toggle to the indices list
+ addToggle(toggle: IndexToggle): void;
+ // adds a column to display additional information added via a data enricher
+ addColumn(column: IndicesListColumn): void;
+ // set the content to render when the indices list is empty
+ setEmptyListContent(content: EmptyListContent): void;
+ // adds a tab to the index details page
+ addIndexDetailsTab(tab: IndexDetailsTab): void;
+ // sets content to render instead of the code block on the overview tab of the index page
+ setIndexOverviewContent(content: IndexContent): void;
+ // sets content to render below the docs link on the mappings tab of the index page
+ setIndexMappingsContent(content: IndexContent): void;
+}
diff --git a/x-pack/packages/index-management/src/services/index.ts b/x-pack/packages/index-management/src/services/index.ts
new file mode 100644
index 0000000000000..1a3d90ef568e2
--- /dev/null
+++ b/x-pack/packages/index-management/src/services/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+export * from './extensions_service';
+export * from './public_api_service';
diff --git a/x-pack/packages/index-management/src/services/public_api_service.ts b/x-pack/packages/index-management/src/services/public_api_service.ts
new file mode 100644
index 0000000000000..d09bda320c9f0
--- /dev/null
+++ b/x-pack/packages/index-management/src/services/public_api_service.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 { EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types';
+import { SendRequestResponse } from '../types';
+
+export interface SerializedEnrichPolicy {
+ type: EnrichPolicyType;
+ name: string;
+ sourceIndices: string[];
+ matchField: string;
+ enrichFields: string[];
+ query?: Record;
+}
+
+export interface PublicApiServiceSetup {
+ getAllEnrichPolicies(): Promise>;
+}
diff --git a/x-pack/packages/index-management/src/types.ts b/x-pack/packages/index-management/src/types.ts
new file mode 100644
index 0000000000000..046693303cddf
--- /dev/null
+++ b/x-pack/packages/index-management/src/types.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 {
+ IlmExplainLifecycleLifecycleExplain,
+ HealthStatus,
+ IndicesStatsIndexMetadataState,
+ Uuid,
+} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
+import type { ManagementSetup } from '@kbn/management-plugin/public';
+import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
+import type { CloudSetup } from '@kbn/cloud-plugin/public';
+import type { ConsolePluginStart } from '@kbn/console-plugin/public';
+import type { ScopedHistory } from '@kbn/core-application-browser';
+import { ExtensionsSetup } from './services/extensions_service';
+import { PublicApiServiceSetup } from './services/public_api_service';
+
+export interface IndexManagementPluginSetup {
+ apiService: PublicApiServiceSetup;
+ extensionsService: ExtensionsSetup;
+}
+
+export interface IndexManagementPluginStart {
+ extensionsService: ExtensionsSetup;
+ getIndexMappingComponent: (deps: {
+ history: ScopedHistory;
+ }) => React.FC;
+}
+
+export interface SetupDependencies {
+ fleet?: unknown;
+ usageCollection: UsageCollectionSetup;
+ management: ManagementSetup;
+ share: SharePluginSetup;
+ cloud?: CloudSetup;
+}
+
+export interface StartDependencies {
+ cloud?: CloudSetup;
+ console?: ConsolePluginStart;
+ share: SharePluginStart;
+ fleet?: unknown;
+ usageCollection: UsageCollectionSetup;
+ management: ManagementSetup;
+}
+
+export interface Index {
+ name: string;
+ primary?: number | string;
+ replica?: number | string;
+ isFrozen: boolean;
+ hidden: boolean;
+ aliases: string | string[];
+ data_stream?: string;
+
+ // The types below are added by extension services if corresponding plugins are enabled (ILM, Rollup, CCR)
+ isRollupIndex?: boolean;
+ ilm?: IlmExplainLifecycleLifecycleExplain;
+ isFollowerIndex?: boolean;
+
+ // The types from here below represent information returned from the index stats API;
+ // treated optional as the stats API is not available on serverless
+ health?: HealthStatus;
+ status?: IndicesStatsIndexMetadataState;
+ uuid?: Uuid;
+ documents?: number;
+ size?: string;
+ primary_size?: string;
+ documents_deleted?: number;
+}
+
+export interface IndexMappingProps {
+ index?: Index;
+ showAboutMappings?: boolean;
+}
+
+export interface SendRequestResponse {
+ data: D | null;
+ error: E | null;
+}
diff --git a/x-pack/packages/index-management/tsconfig.json b/x-pack/packages/index-management/tsconfig.json
new file mode 100644
index 0000000000000..6531510aba657
--- /dev/null
+++ b/x-pack/packages/index-management/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node",
+ "react"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/core-application-browser",
+ "@kbn/usage-collection-plugin",
+ "@kbn/management-plugin",
+ "@kbn/share-plugin",
+ "@kbn/cloud-plugin",
+ "@kbn/console-plugin",
+ ]
+}
diff --git a/x-pack/plugins/cases/public/components/create/description.test.tsx b/x-pack/plugins/cases/public/components/create/description.test.tsx
index d0426731f97d9..5acd5a3b4f5c8 100644
--- a/x-pack/plugins/cases/public/components/create/description.test.tsx
+++ b/x-pack/plugins/cases/public/components/create/description.test.tsx
@@ -15,7 +15,7 @@ import type { AppMockRenderer } from '../../common/mock';
import { createAppMockRenderer } from '../../common/mock';
import { MAX_DESCRIPTION_LENGTH } from '../../../common/constants';
import { FormTestComponent } from '../../common/test_utils';
-import type { FormSchema } from '@kbn/index-management-plugin/public/shared_imports';
+import type { FormSchema } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
describe('Description', () => {
let appMockRender: AppMockRenderer;
diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json
index e493964cc088e..bf205c79e50c8 100644
--- a/x-pack/plugins/cases/tsconfig.json
+++ b/x-pack/plugins/cases/tsconfig.json
@@ -11,12 +11,10 @@
],
"kbn_references": [
"@kbn/core",
-
// optionalPlugins from ./kibana.json
"@kbn/lens-plugin",
"@kbn/security-plugin",
"@kbn/spaces-plugin",
-
// Required from './kibana.json'
"@kbn/actions-plugin",
"@kbn/rule-registry-plugin",
@@ -68,7 +66,6 @@
"@kbn/core-http-server",
"@kbn/alerting-plugin",
"@kbn/content-management-plugin",
- "@kbn/index-management-plugin",
"@kbn/rison",
"@kbn/core-application-browser",
"@kbn/react-kibana-context-render",
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts
index dbf0bb4208911..91a23a42f6aea 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts
@@ -22,7 +22,7 @@ import {
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
-import { IndexMappingProps } from '@kbn/index-management-plugin/public';
+import { IndexMappingProps } from '@kbn/index-management';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import { MlPluginStart } from '@kbn/ml-plugin/public';
import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants';
diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts
index 0a1e4dfd35fbd..754b4ecd401f9 100644
--- a/x-pack/plugins/enterprise_search/public/plugin.ts
+++ b/x-pack/plugins/enterprise_search/public/plugin.ts
@@ -25,7 +25,7 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { i18n } from '@kbn/i18n';
-import { IndexManagementPluginStart } from '@kbn/index-management-plugin/public';
+import type { IndexManagementPluginStart } from '@kbn/index-management';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { MlPluginStart } from '@kbn/ml-plugin/public';
diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json
index 0b3380fd34f9d..2750299a8f6a0 100644
--- a/x-pack/plugins/enterprise_search/tsconfig.json
+++ b/x-pack/plugins/enterprise_search/tsconfig.json
@@ -72,7 +72,7 @@
"@kbn/search-connectors-plugin",
"@kbn/search-playground",
"@kbn/utility-types",
- "@kbn/index-management-plugin",
+ "@kbn/index-management",
"@kbn/deeplinks-search"
]
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx
index 0afe886bf1706..1d828a758b277 100644
--- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx
@@ -26,8 +26,7 @@ import {
import { euiThemeVars } from '@kbn/ui-theme';
import { ApplicationStart } from '@kbn/core/public';
-import { Index } from '@kbn/index-management-plugin/common';
-import { IndexDetailsTab } from '@kbn/index-management-plugin/common/constants';
+import { Index, IndexDetailsTab } from '@kbn/index-management';
import { IlmExplainLifecycleLifecycleExplainManaged } from '@elastic/elasticsearch/lib/api/types';
import { Phase } from '../../../common/types';
import { getPolicyEditPath } from '../../application/services/navigation';
diff --git a/x-pack/plugins/index_lifecycle_management/tsconfig.json b/x-pack/plugins/index_lifecycle_management/tsconfig.json
index 6632d318e5d9f..b759a9468861f 100644
--- a/x-pack/plugins/index_lifecycle_management/tsconfig.json
+++ b/x-pack/plugins/index_lifecycle_management/tsconfig.json
@@ -36,7 +36,8 @@
"@kbn/config-schema",
"@kbn/shared-ux-router",
"@kbn/ui-theme",
- "@kbn/shared-ux-link-redirect-app"
+ "@kbn/shared-ux-link-redirect-app",
+ "@kbn/index-management"
],
"exclude": [
"target/**/*",
diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts
index 718775ed9167e..146c33d6cfd11 100644
--- a/x-pack/plugins/index_management/common/constants/index.ts
+++ b/x-pack/plugins/index_management/common/constants/index.ts
@@ -52,6 +52,6 @@ export {
export { MAJOR_VERSION } from './plugin';
-export { Section, IndexDetailsSection } from './home_sections';
-export type { IndexDetailsTab, IndexDetailsTabId } from './home_sections';
+export { Section, IndexDetailsSection } from '@kbn/index-management';
+export type { IndexDetailsTab, IndexDetailsTabId } from '@kbn/index-management';
export * from './allow_auto_create';
diff --git a/x-pack/plugins/index_management/common/lib/enrich_policies.ts b/x-pack/plugins/index_management/common/lib/enrich_policies.ts
index 9372578089c84..fc7e15addec86 100644
--- a/x-pack/plugins/index_management/common/lib/enrich_policies.ts
+++ b/x-pack/plugins/index_management/common/lib/enrich_policies.ts
@@ -6,7 +6,7 @@
*/
import type { EnrichSummary, EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types';
-import type { SerializedEnrichPolicy } from '../types';
+import type { SerializedEnrichPolicy } from '@kbn/index-management';
export const getPolicyType = (policy: EnrichSummary): EnrichPolicyType => {
if (policy.config.match) {
diff --git a/x-pack/plugins/index_management/common/types/enrich_policies.ts b/x-pack/plugins/index_management/common/types/enrich_policies.ts
index 52bab8e6c0f18..ae516b0775f8f 100644
--- a/x-pack/plugins/index_management/common/types/enrich_policies.ts
+++ b/x-pack/plugins/index_management/common/types/enrich_policies.ts
@@ -5,17 +5,6 @@
* 2.0.
*/
-import type { EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types';
-
-export interface SerializedEnrichPolicy {
- type: EnrichPolicyType;
- name: string;
- sourceIndices: string[];
- matchField: string;
- enrichFields: string[];
- query?: Record;
-}
-
export interface FieldItem {
name: string;
type: string;
diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts
index b5f70ec5e1463..678d5c854470d 100644
--- a/x-pack/plugins/index_management/common/types/indices.ts
+++ b/x-pack/plugins/index_management/common/types/indices.ts
@@ -5,12 +5,7 @@
* 2.0.
*/
-import {
- HealthStatus,
- IlmExplainLifecycleLifecycleExplain,
- IndicesStatsIndexMetadataState,
- Uuid,
-} from '@elastic/elasticsearch/lib/api/types';
+export type { Index } from '@kbn/index-management';
interface IndexModule {
number_of_shards: number | string;
@@ -58,31 +53,6 @@ export interface IndexSettings {
[key: string]: any;
}
-export interface Index {
- name: string;
- primary?: number | string;
- replica?: number | string;
- isFrozen: boolean;
- hidden: boolean;
- aliases: string | string[];
- data_stream?: string;
-
- // The types below are added by extension services if corresponding plugins are enabled (ILM, Rollup, CCR)
- isRollupIndex?: boolean;
- ilm?: IlmExplainLifecycleLifecycleExplain;
- isFollowerIndex?: boolean;
-
- // The types from here below represent information returned from the index stats API;
- // treated optional as the stats API is not available on serverless
- health?: HealthStatus;
- status?: IndicesStatsIndexMetadataState;
- uuid?: Uuid;
- documents?: number;
- size?: string;
- primary_size?: string;
- documents_deleted?: number;
-}
-
export interface IndexSettingsResponse {
settings: IndexSettings;
defaults: IndexSettings;
diff --git a/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx b/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx
index 6f7c79724763e..7f5b3f4b4b7d5 100644
--- a/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx
+++ b/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx
@@ -8,8 +8,8 @@
import React from 'react';
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
-import { ExtensionsService } from '../../../services';
import { CreateIndexButton } from '../../sections/home/index_list/create_index/create_index_button';
+import { ExtensionsService } from '../../../services/extensions_service';
export const NoMatch = ({
loadIndices,
diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts
index aaf2e871ca6f0..ae202bcf55c0f 100644
--- a/x-pack/plugins/index_management/public/application/mount_management_section.ts
+++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts
@@ -13,10 +13,9 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { CloudSetup } from '@kbn/cloud-plugin/public';
import { Observable } from 'rxjs';
+import { StartDependencies } from '@kbn/index-management';
import { UIM_APP_NAME } from '../../common/constants';
import { PLUGIN } from '../../common/constants/plugin';
-import { ExtensionsService } from '../services';
-import { StartDependencies } from '../types';
import { AppDependencies } from './app_context';
import { breadcrumbService } from './services/breadcrumbs';
import { documentationService } from './services/documentation';
@@ -26,6 +25,7 @@ import { renderApp } from '.';
import { setUiMetricService } from './services/api';
import { notificationService } from './services/notification';
import { httpService } from './services/http';
+import { ExtensionsService } from '../services/extensions_service';
function initSetup({
usageCollection,
diff --git a/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/create_policy_context.tsx b/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/create_policy_context.tsx
index a84d503758c6a..dab8056486405 100644
--- a/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/create_policy_context.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/create_policy_context.tsx
@@ -6,7 +6,7 @@
*/
import React, { createContext, useContext, useState } from 'react';
-import type { SerializedEnrichPolicy } from '../../../../common';
+import type { SerializedEnrichPolicy } from '@kbn/index-management';
export type DraftPolicy = Partial;
diff --git a/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/create_policy_wizard.tsx b/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/create_policy_wizard.tsx
index fc5aa6f667a6f..ff20f78830476 100644
--- a/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/create_policy_wizard.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/create_policy_wizard.tsx
@@ -9,12 +9,12 @@ import React, { useState, useMemo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSteps, EuiStepStatus, EuiCallOut, EuiSpacer } from '@elastic/eui';
+import type { SerializedEnrichPolicy } from '@kbn/index-management';
import { useAppContext } from '../../app_context';
import { ConfigurationStep, FieldSelectionStep, CreateStep } from './steps';
import { useCreatePolicyContext } from './create_policy_context';
import { createEnrichPolicy } from '../../services/api';
import type { Error } from '../../../shared_imports';
-import type { SerializedEnrichPolicy } from '../../../../common';
const CONFIGURATION = 1;
const FIELD_SELECTION = 2;
diff --git a/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/steps/create.tsx b/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/steps/create.tsx
index d2226a0adf371..7150e2bd29bac 100644
--- a/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/steps/create.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/enrich_policy_create/steps/create.tsx
@@ -21,7 +21,7 @@ import {
EuiCodeBlock,
} from '@elastic/eui';
-import type { SerializedEnrichPolicy } from '../../../../../common';
+import type { SerializedEnrichPolicy } from '@kbn/index-management';
import { useCreatePolicyContext } from '../create_policy_context';
import { serializeAsESPolicy, getESPolicyCreationApiCall } from '../../../../../common/lib';
diff --git a/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/details_flyout/policy_details_flyout.tsx b/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/details_flyout/policy_details_flyout.tsx
index 7b7bc588e598a..9fb20e33e22c8 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/details_flyout/policy_details_flyout.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/details_flyout/policy_details_flyout.tsx
@@ -21,7 +21,7 @@ import {
EuiButtonEmpty,
} from '@elastic/eui';
import { CodeEditor } from '@kbn/code-editor';
-import type { SerializedEnrichPolicy } from '../../../../../../common';
+import type { SerializedEnrichPolicy } from '@kbn/index-management';
export interface Props {
policy: SerializedEnrichPolicy;
diff --git a/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/enrich_policies_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/enrich_policies_list.tsx
index 0d5c79612191a..e11c0e9db87fc 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/enrich_policies_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/enrich_policies_list.tsx
@@ -12,8 +12,8 @@ import { RouteComponentProps } from 'react-router-dom';
import { Location } from 'history';
import { parse } from 'query-string';
+import type { SerializedEnrichPolicy } from '@kbn/index-management';
import { APP_WRAPPER_CLASS, useExecutionContext } from '../../../../shared_imports';
-import type { SerializedEnrichPolicy } from '../../../../../common';
import { useAppContext } from '../../../app_context';
import { useRedirectPath } from '../../../hooks/redirect_path';
diff --git a/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/policies_table/policies_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/policies_table/policies_table.tsx
index 87002e6042270..50140898ee090 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/policies_table/policies_table.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/enrich_policies_list/policies_table/policies_table.tsx
@@ -16,8 +16,8 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
+import type { SerializedEnrichPolicy } from '@kbn/index-management';
import { useAppContext } from '../../../../app_context';
-import type { SerializedEnrichPolicy } from '../../../../../../common/types';
export interface Props {
policies: SerializedEnrichPolicy[];
diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context_types.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context_types.tsx
index 3d33b77bbd25b..5dd264d38b80e 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context_types.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context_types.tsx
@@ -6,35 +6,9 @@
*/
import { CoreStart } from '@kbn/core/public';
-import { ExtensionsService } from '../../../../../services';
+import { IndexMappingProps } from '@kbn/index-management';
import { AppDependencies } from '../../../../app_context';
-
-// duplicating this Index interface here so we don't blow up the bundle by importing all the types
-interface Index {
- name: string;
- primary?: number | string;
- replica?: number | string;
- isFrozen: boolean;
- hidden: boolean;
- aliases: string | string[];
- data_stream?: string;
-
- // The types below are added by extension services if corresponding plugins are enabled (ILM, Rollup, CCR)
- isRollupIndex?: boolean;
- isFollowerIndex?: boolean;
-
- // The types from here below represent information returned from the index stats API;
- // treated optional as the stats API is not available on serverless
- documents?: number;
- size?: string;
- primary_size?: string;
- documents_deleted?: number;
-}
-
-export interface IndexMappingProps {
- index?: Index;
- showAboutMappings?: boolean;
-}
+import { ExtensionsService } from '../../../../../services/extensions_service';
export type IndexMappingWithContextProps = {
core: CoreStart;
diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts
index eb7e0c1876240..f9a454af4b1a8 100644
--- a/x-pack/plugins/index_management/public/application/services/api.ts
+++ b/x-pack/plugins/index_management/public/application/services/api.ts
@@ -7,6 +7,7 @@
import { METRIC_TYPE } from '@kbn/analytics';
import { IndicesStatsResponse } from '@elastic/elasticsearch/lib/api/types';
+import type { SerializedEnrichPolicy } from '@kbn/index-management';
import {
API_BASE_PATH,
INTERNAL_API_BASE_PATH,
@@ -44,7 +45,7 @@ import {
import { useRequest, sendRequest } from './use_request';
import { httpService } from './http';
import { UiMetricService } from './ui_metric';
-import type { SerializedEnrichPolicy, FieldFromIndicesRequest } from '../../../common';
+import type { FieldFromIndicesRequest } from '../../../common';
import { Fields } from '../components/mappings_editor/types';
interface ReloadIndicesOptions {
diff --git a/x-pack/plugins/index_management/public/index.ts b/x-pack/plugins/index_management/public/index.ts
index 19d50a5f961f8..09adb299c711f 100644
--- a/x-pack/plugins/index_management/public/index.ts
+++ b/x-pack/plugins/index_management/public/index.ts
@@ -14,10 +14,11 @@ export const plugin = (ctx: PluginInitializerContext) => {
return new IndexMgmtUIPlugin(ctx);
};
-export type { IndexManagementPluginSetup, IndexManagementPluginStart } from './types';
+export type {
+ Index,
+ IndexMappingProps,
+ IndexManagementPluginSetup,
+ IndexManagementPluginStart,
+} from '@kbn/index-management';
export { getIndexListUri, getTemplateDetailsLink } from './application/services/routing';
-
-export type { Index } from '../common';
-
-export type { IndexMappingProps } from './application/sections/home/index_list/details_page/index_mapping_with_context_types';
diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts
index 4cc904ebf6ad0..d733c4dfeeeb2 100644
--- a/x-pack/plugins/index_management/public/plugin.ts
+++ b/x-pack/plugins/index_management/public/plugin.ts
@@ -15,21 +15,21 @@ import {
PluginInitializerContext,
ScopedHistory,
} from '@kbn/core/public';
-import { setExtensionsService } from './application/store/selectors/extension_service';
-
-import { ExtensionsService, PublicApiService } from './services';
-
import {
IndexManagementPluginSetup,
SetupDependencies,
StartDependencies,
- ClientConfigType,
IndexManagementPluginStart,
-} from './types';
+} from '@kbn/index-management';
+import { setExtensionsService } from './application/store/selectors/extension_service';
+import { ExtensionsService } from './services/extensions_service';
+
+import { ClientConfigType } from './types';
// avoid import from index files in plugin.ts, use specific import paths
import { PLUGIN } from '../common/constants/plugin';
import { IndexMapping } from './application/sections/home/index_list/details_page/index_mappings_embeddable';
+import { PublicApiService } from './services/public_api_service';
export class IndexMgmtUIPlugin
implements
diff --git a/x-pack/plugins/index_management/public/services/extensions_service.mock.ts b/x-pack/plugins/index_management/public/services/extensions_service.mock.ts
index 73053eec98556..072acd92f5a9d 100644
--- a/x-pack/plugins/index_management/public/services/extensions_service.mock.ts
+++ b/x-pack/plugins/index_management/public/services/extensions_service.mock.ts
@@ -6,7 +6,8 @@
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
-import { ExtensionsService, ExtensionsSetup } from './extensions_service';
+import { ExtensionsSetup } from '@kbn/index-management/src/services/extensions_service';
+import { ExtensionsService } from './extensions_service';
export type ExtensionsSetupMock = jest.Mocked;
diff --git a/x-pack/plugins/index_management/public/services/extensions_service.ts b/x-pack/plugins/index_management/public/services/extensions_service.ts
index e7cb4aef2282d..3823ad7ed28f2 100644
--- a/x-pack/plugins/index_management/public/services/extensions_service.ts
+++ b/x-pack/plugins/index_management/public/services/extensions_service.ts
@@ -6,70 +6,15 @@
*/
import { i18n } from '@kbn/i18n';
-import { FunctionComponent, ReactNode } from 'react';
-import { ApplicationStart } from '@kbn/core-application-browser';
-import { EuiBadgeProps } from '@elastic/eui';
-import type { IndexDetailsTab } from '../../common/constants';
-import { Index } from '..';
-
-export interface IndexContent {
- renderContent: (args: {
- index: Index;
- getUrlForApp: ApplicationStart['getUrlForApp'];
- }) => ReturnType;
-}
-
-export interface IndexToggle {
- matchIndex: (index: Index) => boolean;
- label: string;
- name: string;
-}
-export interface IndexBadge {
- matchIndex: (index: Index) => boolean;
- label: string;
- // a parseable search bar filter expression, for example "isFollowerIndex:true"
- filterExpression?: string;
- color: EuiBadgeProps['color'];
-}
-
-export interface EmptyListContent {
- renderContent: (args: {
- // the button to open the "create index" modal
- createIndexButton: ReturnType;
- }) => ReturnType;
-}
-
-export interface IndicesListColumn {
- fieldName: string;
- label: string;
- order: number;
- render?: (index: Index) => ReactNode;
- // return a value used for sorting (only if the value is different from the original value at index[fieldName])
- sort?: (index: Index) => any;
-}
-
-export interface ExtensionsSetup {
- // adds an option to the "manage index" menu
- addAction(action: any): void;
- // adds a banner to the indices list
- addBanner(banner: any): void;
- // adds a filter to the indices list
- addFilter(filter: any): void;
- // adds a badge to the index name
- addBadge(badge: IndexBadge): void;
- // adds a toggle to the indices list
- addToggle(toggle: IndexToggle): void;
- // adds a column to display additional information added via a data enricher
- addColumn(column: IndicesListColumn): void;
- // set the content to render when the indices list is empty
- setEmptyListContent(content: EmptyListContent): void;
- // adds a tab to the index details page
- addIndexDetailsTab(tab: IndexDetailsTab): void;
- // sets content to render instead of the code block on the overview tab of the index page
- setIndexOverviewContent(content: IndexContent): void;
- // sets content to render below the docs link on the mappings tab of the index page
- setIndexMappingsContent(content: IndexContent): void;
-}
+import {
+ IndexBadge,
+ IndexToggle,
+ IndicesListColumn,
+ EmptyListContent,
+ IndexContent,
+ ExtensionsSetup,
+} from '@kbn/index-management';
+import { IndexDetailsTab } from '../../common/constants';
export class ExtensionsService {
private _actions: any[] = [];
diff --git a/x-pack/plugins/index_management/public/services/index.ts b/x-pack/plugins/index_management/public/services/index.ts
index bca35e09c9776..92c6e95a8b432 100644
--- a/x-pack/plugins/index_management/public/services/index.ts
+++ b/x-pack/plugins/index_management/public/services/index.ts
@@ -5,8 +5,6 @@
* 2.0.
*/
-export type { ExtensionsSetup, IndexContent } from './extensions_service';
export { ExtensionsService } from './extensions_service';
-export type { PublicApiServiceSetup } from './public_api_service';
export { PublicApiService } from './public_api_service';
diff --git a/x-pack/plugins/index_management/public/services/public_api_service.mock.ts b/x-pack/plugins/index_management/public/services/public_api_service.mock.ts
index 85ce1b232c06a..567c87b914654 100644
--- a/x-pack/plugins/index_management/public/services/public_api_service.mock.ts
+++ b/x-pack/plugins/index_management/public/services/public_api_service.mock.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { PublicApiServiceSetup } from './public_api_service';
+import { PublicApiServiceSetup } from '@kbn/index-management/src/services/public_api_service';
export type PublicApiServiceSetupMock = jest.Mocked;
diff --git a/x-pack/plugins/index_management/public/services/public_api_service.ts b/x-pack/plugins/index_management/public/services/public_api_service.ts
index 75a3081c0fda5..bf51e90b75d38 100644
--- a/x-pack/plugins/index_management/public/services/public_api_service.ts
+++ b/x-pack/plugins/index_management/public/services/public_api_service.ts
@@ -5,14 +5,9 @@
* 2.0.
*/
-import { HttpSetup } from '@kbn/core/public';
-import { sendRequest, SendRequestResponse } from '../shared_imports';
-import { INTERNAL_API_BASE_PATH } from '../../common/constants';
-import { SerializedEnrichPolicy } from '../../common/types';
-
-export interface PublicApiServiceSetup {
- getAllEnrichPolicies(): Promise>;
-}
+import { HttpSetup } from '@kbn/core-http-browser';
+import { INTERNAL_API_BASE_PATH } from '../../common';
+import { sendRequest } from '../shared_imports';
/**
* Index Management public API service
diff --git a/x-pack/plugins/index_management/public/types.ts b/x-pack/plugins/index_management/public/types.ts
index 7deb650c1c266..9b85903f1884a 100644
--- a/x-pack/plugins/index_management/public/types.ts
+++ b/x-pack/plugins/index_management/public/types.ts
@@ -5,44 +5,6 @@
* 2.0.
*/
-import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
-import { ManagementSetup } from '@kbn/management-plugin/public';
-import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
-import { CloudSetup } from '@kbn/cloud-plugin/public';
-import { ConsolePluginStart } from '@kbn/console-plugin/public';
-import { ScopedHistory } from '@kbn/core-application-browser';
-import { IndexMappingProps } from './application/sections/home/index_list/details_page/index_mapping_with_context_types';
-import { ExtensionsSetup, PublicApiServiceSetup } from './services';
-
-export interface IndexManagementPluginSetup {
- apiService: PublicApiServiceSetup;
- extensionsService: ExtensionsSetup;
-}
-
-export interface IndexManagementPluginStart {
- extensionsService: ExtensionsSetup;
- getIndexMappingComponent: (deps: {
- history: ScopedHistory;
- }) => React.FC;
-}
-
-export interface SetupDependencies {
- fleet?: unknown;
- usageCollection: UsageCollectionSetup;
- management: ManagementSetup;
- share: SharePluginSetup;
- cloud?: CloudSetup;
-}
-
-export interface StartDependencies {
- cloud?: CloudSetup;
- console?: ConsolePluginStart;
- share: SharePluginStart;
- fleet?: unknown;
- usageCollection: UsageCollectionSetup;
- management: ManagementSetup;
-}
-
export interface ClientConfigType {
ui: {
enabled: boolean;
diff --git a/x-pack/plugins/index_management/server/lib/enrich_policies.ts b/x-pack/plugins/index_management/server/lib/enrich_policies.ts
index db25603817e64..61fa1d1467006 100644
--- a/x-pack/plugins/index_management/server/lib/enrich_policies.ts
+++ b/x-pack/plugins/index_management/server/lib/enrich_policies.ts
@@ -7,7 +7,7 @@
import { IScopedClusterClient } from '@kbn/core/server';
import type { EnrichSummary } from '@elastic/elasticsearch/lib/api/types';
-import type { SerializedEnrichPolicy } from '../../common/types';
+import type { SerializedEnrichPolicy } from '@kbn/index-management';
import { getPolicyType } from '../../common/lib';
export const serializeEnrichmentPolicies = (
diff --git a/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_create_route.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_create_route.ts
index 2a26ec687574c..8b43fc19d2a31 100644
--- a/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_create_route.ts
+++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_create_route.ts
@@ -8,12 +8,12 @@
import { IScopedClusterClient } from '@kbn/core/server';
import { schema, TypeOf } from '@kbn/config-schema';
+import type { SerializedEnrichPolicy } from '@kbn/index-management';
import { RouteDependencies } from '../../../types';
import { addInternalBasePath } from '..';
import { enrichPoliciesActions } from '../../../lib/enrich_policies';
import { serializeAsESPolicy } from '../../../../common/lib';
import { normalizeFieldsList, getIndices, FieldCapsList, getCommonFields } from './helpers';
-import type { SerializedEnrichPolicy } from '../../../../common';
const validationSchema = schema.object({
policy: schema.object({
diff --git a/x-pack/plugins/index_management/tsconfig.json b/x-pack/plugins/index_management/tsconfig.json
index 3247b35bec421..dc587d5a1ab9e 100644
--- a/x-pack/plugins/index_management/tsconfig.json
+++ b/x-pack/plugins/index_management/tsconfig.json
@@ -26,7 +26,6 @@
"@kbn/i18n",
"@kbn/ml-trained-models-utils",
"@kbn/analytics",
- "@kbn/utility-types",
"@kbn/i18n-react",
"@kbn/core-doc-links-browser-mocks",
"@kbn/core-execution-context-browser-mocks",
@@ -42,11 +41,14 @@
"@kbn/search-api-panels",
"@kbn/cloud-plugin",
"@kbn/ui-theme",
- "@kbn/core-application-browser",
"@kbn/code-editor",
"@kbn/monaco",
"@kbn/console-plugin",
- "@kbn/shared-ux-utility"
+ "@kbn/shared-ux-utility",
+ "@kbn/index-management",
+ "@kbn/utility-types"
],
- "exclude": ["target/**/*"]
+ "exclude": [
+ "target/**/*"
+ ]
}
diff --git a/x-pack/plugins/serverless_search/public/application/components/index_management/index_mappings_docs_link.tsx b/x-pack/plugins/serverless_search/public/application/components/index_management/index_mappings_docs_link.tsx
index 9949b40e3c63f..d75a96ba45e6f 100644
--- a/x-pack/plugins/serverless_search/public/application/components/index_management/index_mappings_docs_link.tsx
+++ b/x-pack/plugins/serverless_search/public/application/components/index_management/index_mappings_docs_link.tsx
@@ -18,7 +18,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { CoreStart } from '@kbn/core/public';
-import { IndexContent } from '@kbn/index-management-plugin/public/services';
+import { IndexContent } from '@kbn/index-management';
const IndexMappingsDocsLink: FunctionComponent<{ docLinks: CoreStart['docLinks'] }> = ({
docLinks,
diff --git a/x-pack/plugins/serverless_search/public/application/components/index_management/index_overview_content.tsx b/x-pack/plugins/serverless_search/public/application/components/index_management/index_overview_content.tsx
index 373dce2557571..c1d31dd50a20c 100644
--- a/x-pack/plugins/serverless_search/public/application/components/index_management/index_overview_content.tsx
+++ b/x-pack/plugins/serverless_search/public/application/components/index_management/index_overview_content.tsx
@@ -12,7 +12,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { EuiLoadingSpinner } from '@elastic/eui';
-import { IndexContent } from '@kbn/index-management-plugin/public/services';
+import { IndexContent } from '@kbn/index-management';
import { ServerlessSearchPluginStartDependencies } from '../../../types';
diff --git a/x-pack/plugins/serverless_search/tsconfig.json b/x-pack/plugins/serverless_search/tsconfig.json
index 673d7dcb99148..d8762b4156ac2 100644
--- a/x-pack/plugins/serverless_search/tsconfig.json
+++ b/x-pack/plugins/serverless_search/tsconfig.json
@@ -46,5 +46,6 @@
"@kbn/core-logging-server-mocks",
"@kbn/discover-plugin",
"@kbn/search-connectors-plugin",
+ "@kbn/index-management",
]
}
diff --git a/yarn.lock b/yarn.lock
index 19de892abf9c0..4f964c832ebc0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4956,6 +4956,10 @@
version "0.0.0"
uid ""
+"@kbn/index-management@link:x-pack/packages/index-management":
+ version "0.0.0"
+ uid ""
+
"@kbn/index-patterns-test-plugin@link:test/plugin_functional/plugins/index_patterns":
version "0.0.0"
uid ""
From 00a637c8ce0d33df6a12b40a6ab6c765afaf440d Mon Sep 17 00:00:00 2001
From: Walter Rafelsberger
Date: Wed, 10 Apr 2024 17:53:14 +0200
Subject: [PATCH 22/55] [ML] AIOps: Updates README with more details about log
rate analysis. (#180258)
## Summary
- Updates `README.md` in `plugins/aiops` to include more details about
the implementation of log rate analysis. You can view the rendered
markdown
[here](https://github.com/elastic/kibana/blob/86a629780871ea5141b548ce5c7aafc396717288/x-pack/plugins/aiops/README.md).
- Updates debug logging to output both `baseline` and `deviation` doc
count.
### Checklist
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---
x-pack/plugins/aiops/README.md | 16 +++++++++++++---
.../analysis_handlers/index_info_handler.ts | 7 +++----
.../log_rate_analysis/route_handler_factory.ts | 2 +-
3 files changed, 17 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/aiops/README.md b/x-pack/plugins/aiops/README.md
index 2ea2a315c594f..7be0e3b59aa68 100755
--- a/x-pack/plugins/aiops/README.md
+++ b/x-pack/plugins/aiops/README.md
@@ -14,6 +14,16 @@ The route handler sets up `response_stream_factory:responseStreamFactory()` to c
The response stream factory acts as a wrapper to set up the stream itself, the stream state (for example to set if it's running etc.), some custom actions on the stream as well as analysis handlers that fetch data from ES and pass it on to the stream.
-## Development
-
-See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment.
+### Analysis details
+
+Here are some more details on the steps involved to do Log Rate Analysis:
+
+- **Index info**: This gathers information from the selected index to identify which type of analysis will be run and which fields will be used for analysis.
+ - **Zero Docs Fallback**: If there are no docs in either `baseline` or `baseline`, the analysis will not identify statistically significant items but will just run regular `terms` aggregations and return the top items for the deviation time range.
+ - **Field identification**: This runs field caps with the `include_empty_fields=false` option to get populated fields. Custom Kibana code then identifies `keyword/ip/boolean` and `text/match_only/text` fields suitable for analysis. When there's a field with both `keyword/text` mappings the `keyword` one will be preferred unless there's an override defined (currently `message` and `error.message`).
+- **Statistically significant items**:
+ - **General notes**: Both aggregatable fields and log pattern queries will be wrapped in `random_sampler` aggregations . The p-value threshold to define statistically significant items is `0.02`.
+ - **Aggregatable fields**: For this we use the ES `significant_terms` aggregation with the p-value score option (https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-significantterms-aggregation.html#p-value-score). The `baseline` time range is used as the `background_filter`, the `deviation` time range is used for the query part (=foreground).
+ - **Log patterns**: To identify statistically significant entries in text fields there is not an ES equivalent to `significant_terms`, so we cannot run a single query for a field to do this. Instead, we use the following approach: We use the `categorize_text` aggregation to identify top text patterns across the baseline and deviation timerange (not yet statistically significant!). Then, for each identified text pattern, we get the document counts for both baseline and deviation. We then use the retrieved counts to run them against the same Kibana code we use for the Data Drift View to detect if there's a statistically significant difference in the counts (`@kbn/ml-chi2test` package, `x-pack/packages/ml/chi2test/critical_table_lookup.ts`). Text field pattern support was added in 8.11, see [#167467](https://github.com/elastic/kibana/issues/167467) for more details.
+- **Grouping**: The grouping tries to identify co-occurences of identified significant items. Again, we have to take different approaches for aggregatable fields and log patterns, but eventually we combine the results. The `frequent_item_sets` aggregation is used as a first step to get co-occurence stats of aggregatable fields. This can be a heavy aggregation so we limit how many values per field we pass on to the agg (`50` at the moment). For each possible aggregatable field to log pattern relation we query the doc count. The result of the `frequent_item_sets` aggregation and those doc counts get then passed on to custom code (derived but over time slighty improved from the original PoC Python Notebooks) to transform that raw data into groups (`x-pack/packages/ml/aiops_log_rate_analysis/queries/get_significant_item_groups.ts`).
+- **Histogram data**: In addition to the analysis itself the endpoint returns histogram data for the result table sparklines.
diff --git a/x-pack/plugins/aiops/server/routes/log_rate_analysis/analysis_handlers/index_info_handler.ts b/x-pack/plugins/aiops/server/routes/log_rate_analysis/analysis_handlers/index_info_handler.ts
index 2a29aa6c13e81..730b11cdd28a5 100644
--- a/x-pack/plugins/aiops/server/routes/log_rate_analysis/analysis_handlers/index_info_handler.ts
+++ b/x-pack/plugins/aiops/server/routes/log_rate_analysis/analysis_handlers/index_info_handler.ts
@@ -37,7 +37,6 @@ export const indexInfoHandlerFactory =
const textFieldCandidates: string[] = [];
- let totalDocCount = 0;
let zeroDocsFallback = false;
if (!requestBody.overrides?.remainingFieldCandidates) {
@@ -63,10 +62,12 @@ export const indexInfoHandlerFactory =
abortSignal
);
+ logDebugMessage(`Baseline document count: ${indexInfo.baselineTotalDocCount}`);
+ logDebugMessage(`Deviation document count: ${indexInfo.deviationTotalDocCount}`);
+
fieldCandidates.push(...indexInfo.fieldCandidates);
fieldCandidatesCount = fieldCandidates.length;
textFieldCandidates.push(...indexInfo.textFieldCandidates);
- totalDocCount = indexInfo.deviationTotalDocCount;
zeroDocsFallback = indexInfo.zeroDocsFallback;
} catch (e) {
if (!isRequestAbortedError(e)) {
@@ -77,8 +78,6 @@ export const indexInfoHandlerFactory =
return;
}
- logDebugMessage(`Total document count: ${totalDocCount}`);
-
stateHandler.loaded(LOADED_FIELD_CANDIDATES, false);
responseStream.pushPingWithTimeout();
diff --git a/x-pack/plugins/aiops/server/routes/log_rate_analysis/route_handler_factory.ts b/x-pack/plugins/aiops/server/routes/log_rate_analysis/route_handler_factory.ts
index 0d0319e9b1ecf..69b9cc923799d 100644
--- a/x-pack/plugins/aiops/server/routes/log_rate_analysis/route_handler_factory.ts
+++ b/x-pack/plugins/aiops/server/routes/log_rate_analysis/route_handler_factory.ts
@@ -81,7 +81,7 @@ export function routeHandlerFactory(
analysis.overridesHandler();
responseStream.pushPingWithTimeout();
- // Step 1: Index Info: Field candidates, total doc count, sample probability
+ // Step 1: Index Info: Field candidates and zero docs fallback flag
const indexInfo = await analysis.indexInfoHandler();
if (!indexInfo) {
From 756a41b2d27b1320e8ed25cffdbc8fbd9eead332 Mon Sep 17 00:00:00 2001
From: Walter Rafelsberger
Date: Wed, 10 Apr 2024 18:20:41 +0200
Subject: [PATCH 23/55] [ML] Transforms: Use
`estypes.TransformGetTransformStatsTransformStats` for transform stats.
(#180347)
## Summary
Gets rid of a lot of the manually defined attributes of `TransformStats`
by extending from `estypes.TransformGetTransformStatsTransformStats`.
How we treat the transform health status needed to be adapted because
the types provided by `estypes` are both lower and upper case (e.g.
`green/GREEN`) but lack the `unknown` status we use in the UI.
`mapEsHealthStatus2TransformHealthStatus()` is used to transform a
health status returned via API to the one we use in the UI.
### 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
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: Dima Arnautov
---
.../transform/common/constants.test.ts | 20 ++++++
x-pack/plugins/transform/common/constants.ts | 18 ++++--
.../transform/common/types/transform_stats.ts | 61 +++----------------
.../app/common/reauthorization_utils.ts | 9 ++-
.../transform_list/expanded_row.tsx | 9 ++-
.../expanded_row_health_pane.tsx | 7 ++-
.../transform_health_colored_dot.tsx | 4 +-
.../transform_search_bar_filters.tsx | 4 +-
.../components/transform_list/use_columns.tsx | 13 ++--
.../register_transform_health_rule_type.ts | 4 +-
.../transform_health_service.ts | 12 +++-
.../apis/transform/reauthorize_transforms.ts | 12 ++--
.../apps/transform/actions/starting.ts | 18 +++---
13 files changed, 101 insertions(+), 90 deletions(-)
create mode 100644 x-pack/plugins/transform/common/constants.test.ts
diff --git a/x-pack/plugins/transform/common/constants.test.ts b/x-pack/plugins/transform/common/constants.test.ts
new file mode 100644
index 0000000000000..50d6aceb12e58
--- /dev/null
+++ b/x-pack/plugins/transform/common/constants.test.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.
+ */
+
+import { mapEsHealthStatus2TransformHealthStatus } from './constants';
+
+describe('mapEsHealthStatus2TransformHealthStatus', () => {
+ it('maps estypes HealthStatus to TransformHealthStatus', () => {
+ expect(mapEsHealthStatus2TransformHealthStatus(undefined)).toBe('unknown');
+ expect(mapEsHealthStatus2TransformHealthStatus('green')).toBe('green');
+ expect(mapEsHealthStatus2TransformHealthStatus('GREEN')).toBe('green');
+ expect(mapEsHealthStatus2TransformHealthStatus('yellow')).toBe('yellow');
+ expect(mapEsHealthStatus2TransformHealthStatus('YELLOW')).toBe('yellow');
+ expect(mapEsHealthStatus2TransformHealthStatus('red')).toBe('red');
+ expect(mapEsHealthStatus2TransformHealthStatus('RED')).toBe('red');
+ });
+});
diff --git a/x-pack/plugins/transform/common/constants.ts b/x-pack/plugins/transform/common/constants.ts
index bc6e87295ea7a..761d9e4c79c72 100644
--- a/x-pack/plugins/transform/common/constants.ts
+++ b/x-pack/plugins/transform/common/constants.ts
@@ -5,8 +5,9 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
+import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { i18n } from '@kbn/i18n';
import type { LicenseType } from '@kbn/licensing-plugin/common/types';
import { ALERT_NAMESPACE } from '@kbn/rule-data-utils';
import type { TransformHealthTests } from './types/alerting';
@@ -103,14 +104,21 @@ export const TRANSFORM_STATE = {
export type TransformState = typeof TRANSFORM_STATE[keyof typeof TRANSFORM_STATE];
-export const TRANSFORM_HEALTH = {
+export const TRANSFORM_HEALTH_STATUS = {
green: 'green',
- unknown: 'unknown',
yellow: 'yellow',
red: 'red',
+ unknown: 'unknown',
} as const;
-
-export type TransformHealth = typeof TRANSFORM_HEALTH[keyof typeof TRANSFORM_HEALTH];
+export type TransformHealthStatus = keyof typeof TRANSFORM_HEALTH_STATUS;
+export const isTransformHealthStatus = (arg: unknown): arg is TransformHealthStatus =>
+ typeof arg === 'string' && Object.keys(TRANSFORM_HEALTH_STATUS).includes(arg);
+export const mapEsHealthStatus2TransformHealthStatus = (
+ healthStatus?: estypes.HealthStatus
+): TransformHealthStatus =>
+ typeof healthStatus === 'string' && isTransformHealthStatus(healthStatus.toLowerCase())
+ ? (healthStatus.toLowerCase() as TransformHealthStatus)
+ : TRANSFORM_HEALTH_STATUS.unknown;
export const TRANSFORM_HEALTH_COLOR = {
green: 'success',
diff --git a/x-pack/plugins/transform/common/types/transform_stats.ts b/x-pack/plugins/transform/common/types/transform_stats.ts
index a250616da8a73..5b846055dc941 100644
--- a/x-pack/plugins/transform/common/types/transform_stats.ts
+++ b/x-pack/plugins/transform/common/types/transform_stats.ts
@@ -5,10 +5,11 @@
* 2.0.
*/
+import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
-import { type TransformHealth, type TransformState, TRANSFORM_STATE } from '../constants';
-import type { TransformId } from './transform';
+import { type TransformState, TRANSFORM_STATE } from '../constants';
export interface TransformHealthIssue {
type: string;
@@ -18,56 +19,12 @@ export interface TransformHealthIssue {
first_occurrence?: number;
}
-export interface TransformStats {
- id: TransformId;
- checkpointing: {
- last: {
- checkpoint: number;
- timestamp_millis?: number;
- };
- next?: {
- checkpoint: number;
- checkpoint_progress?: {
- total_docs: number;
- docs_remaining: number;
- percent_complete: number;
- };
- };
- changes_last_detected_at: number;
- last_search_time?: number;
- operations_behind?: number;
- };
- health: {
- status: TransformHealth;
- issues?: TransformHealthIssue[];
- };
- node?: {
- id: string;
- name: string;
- ephemeral_id: string;
- transport_address: string;
- attributes: Record;
- };
- stats: {
- delete_time_in_ms: number;
- documents_deleted: number;
- documents_indexed: number;
- documents_processed: number;
- index_failures: number;
- index_time_in_ms: number;
- index_total: number;
- pages_processed: number;
- search_failures: number;
- search_time_in_ms: number;
- search_total: number;
- trigger_count: number;
- processing_time_in_ms: number;
- processing_total: number;
- exponential_avg_checkpoint_duration_ms: number;
- exponential_avg_documents_indexed: number;
- exponential_avg_documents_processed: number;
- };
- reason?: string;
+export interface TransformHealth extends estypes.TransformGetTransformStatsTransformStatsHealth {
+ issues?: TransformHealthIssue[];
+}
+
+export interface TransformStats extends estypes.TransformGetTransformStatsTransformStats {
+ health?: TransformHealth;
state: TransformState;
}
diff --git a/x-pack/plugins/transform/public/app/common/reauthorization_utils.ts b/x-pack/plugins/transform/public/app/common/reauthorization_utils.ts
index d32383ccd683e..26dee745a4b99 100644
--- a/x-pack/plugins/transform/public/app/common/reauthorization_utils.ts
+++ b/x-pack/plugins/transform/public/app/common/reauthorization_utils.ts
@@ -7,14 +7,19 @@
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import type { TransformHealthIssue } from '../../../common/types/transform_stats';
-import { TRANSFORM_HEALTH } from '../../../common/constants';
+import {
+ mapEsHealthStatus2TransformHealthStatus,
+ TRANSFORM_HEALTH_STATUS,
+} from '../../../common/constants';
import type { TransformListRow } from './transform_list';
export const needsReauthorization = (transform: Partial) => {
return (
isPopulatedObject(transform.config?.authorization, ['api_key']) &&
isPopulatedObject(transform.stats) &&
- transform.stats.health.status === TRANSFORM_HEALTH.red &&
+ isPopulatedObject(transform.stats.health) &&
+ mapEsHealthStatus2TransformHealthStatus(transform.stats.health.status) ===
+ TRANSFORM_HEALTH_STATUS.red &&
transform.stats.health.issues?.find(
(issue) => (issue as TransformHealthIssue).issue === 'Privileges check failed'
) !== undefined
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx
index 49ce00c80cbf3..5abf0c891641c 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx
@@ -25,6 +25,9 @@ import { stringHash } from '@kbn/ml-string-hash';
import { isDefined } from '@kbn/ml-is-defined';
import { FormattedMessage } from '@kbn/i18n-react';
+
+import { mapEsHealthStatus2TransformHealthStatus } from '../../../../../../common/constants';
+
import { useEnabledFeatures } from '../../../../serverless_context';
import { isTransformListRowWithStats } from '../../../../common/transform_list';
import type { TransformHealthAlertRule } from '../../../../../../common/types/alerting';
@@ -154,7 +157,11 @@ export const ExpandedRow: FC = ({ item, onAlertEdit, transformsStatsLoadi
if (item.stats.health !== undefined) {
stateItems.push({
title: 'health',
- description: ,
+ description: (
+
+ ),
});
}
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_health_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_health_pane.tsx
index 3a088d5a9b273..2741efc8b143f 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_health_pane.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_health_pane.tsx
@@ -17,6 +17,8 @@ import type {
TransformStats,
} from '../../../../../../common/types/transform_stats';
+import { mapEsHealthStatus2TransformHealthStatus } from '../../../../../../common/constants';
+
import { TransformHealthColoredDot } from './transform_health_colored_dot';
interface ExpandedRowHealthPaneProps {
@@ -24,7 +26,8 @@ interface ExpandedRowHealthPaneProps {
}
export const ExpandedRowHealthPane: FC = ({ health }) => {
- const { status, issues } = health;
+ const healthStatus = mapEsHealthStatus2TransformHealthStatus(health?.status);
+ const issues = health?.issues;
const sorting = {
sort: {
@@ -80,7 +83,7 @@ export const ExpandedRowHealthPane: FC = ({ health }
data-test-subj="transformHealthTabContent"
>
-
+
{Array.isArray(issues) && issues.length > 0 && (
<>
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_health_colored_dot.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_health_colored_dot.tsx
index 632e5ee1c6bbd..9d151ca7b8deb 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_health_colored_dot.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_health_colored_dot.tsx
@@ -10,14 +10,14 @@ import React, { type FC } from 'react';
import { EuiHealth, EuiToolTip } from '@elastic/eui';
import {
- type TransformHealth,
+ type TransformHealthStatus,
TRANSFORM_HEALTH_COLOR,
TRANSFORM_HEALTH_DESCRIPTION,
TRANSFORM_HEALTH_LABEL,
} from '../../../../../../common/constants';
interface TransformHealthProps {
- healthStatus: TransformHealth;
+ healthStatus: TransformHealthStatus;
compact?: boolean;
showToolTip?: boolean;
}
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx
index 09fb5cb049ac4..c60c302d86e06 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx
@@ -14,7 +14,7 @@ import {
TRANSFORM_FUNCTION,
TRANSFORM_MODE,
TRANSFORM_STATE,
- TRANSFORM_HEALTH,
+ TRANSFORM_HEALTH_STATUS,
} from '../../../../../../common/constants';
import { isLatestTransform, isPivotTransform } from '../../../../../../common/types/transform';
import type { TransformListRow } from '../../../../common';
@@ -53,7 +53,7 @@ export const transformFilters: SearchFilterConfig[] = [
field: 'health',
name: i18n.translate('xpack.transform.healthFilter', { defaultMessage: 'Health' }),
multiSelect: false,
- options: Object.values(TRANSFORM_HEALTH).map((val) => ({
+ options: Object.values(TRANSFORM_HEALTH_STATUS).map((val) => ({
value: val,
name: val,
view: ,
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx
index 55941929009b3..5cd27d1c2fd16 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx
@@ -31,7 +31,10 @@ import { useTransformCapabilities } from '../../../../hooks';
import { needsReauthorization } from '../../../../common/reauthorization_utils';
import type { TransformId } from '../../../../../../common/types/transform';
import { isLatestTransform, isPivotTransform } from '../../../../../../common/types/transform';
-import { TRANSFORM_STATE } from '../../../../../../common/constants';
+import {
+ mapEsHealthStatus2TransformHealthStatus,
+ TRANSFORM_STATE,
+} from '../../../../../../common/constants';
import type { TransformListRow } from '../../../../common';
import { getTransformProgress, TRANSFORM_LIST_COLUMN } from '../../../../common';
@@ -349,11 +352,13 @@ export const useColumns = (
{
name: i18n.translate('xpack.transform.health', { defaultMessage: 'Health' }),
'data-test-subj': 'transformListColumnHealth',
- sortable: (item: TransformListRow) => item.stats?.health.status,
+ sortable: (item: TransformListRow) => item.stats?.health?.status,
truncateText: true,
render(item: TransformListRow) {
- return item.stats ? (
-
+ return item.stats?.health ? (
+
) : (
);
diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts
index dc4a9391bdeac..ac04ff6a7662e 100644
--- a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts
+++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts
@@ -27,7 +27,7 @@ import { ALERT_REASON } from '@kbn/rule-data-utils';
import { ES_FIELD_TYPES } from '@kbn/field-types';
import {
PLUGIN,
- type TransformHealth,
+ type TransformHealthStatus,
TRANSFORM_RULE_TYPE,
TRANSFORM_HEALTH_RESULTS,
} from '../../../../common/constants';
@@ -38,7 +38,7 @@ import { transformHealthServiceProvider } from './transform_health_service';
export interface BaseTransformAlertResponse {
transform_id: string;
description?: string;
- health_status: TransformHealth;
+ health_status: TransformHealthStatus;
issues?: Array<{ issue: string; details?: string; count: number; first_occurrence?: string }>;
}
diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts
index 313717e54debb..48d411feeae78 100644
--- a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts
+++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts
@@ -13,8 +13,10 @@ import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common';
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import type { TransformStats } from '../../../../common/types/transform_stats';
+import { TRANSFORM_HEALTH_STATUS } from '../../../../common/constants';
import type { TransformHealthRuleParams } from './schema';
import {
+ mapEsHealthStatus2TransformHealthStatus,
ALL_TRANSFORMS_SELECTION,
TRANSFORM_HEALTH_CHECK_NAMES,
TRANSFORM_NOTIFICATIONS_INDEX,
@@ -116,8 +118,8 @@ export function transformHealthServiceProvider({
description: transformsDict.get(transformStats.id)?.description,
transform_state: transformStats.state,
node_name: transformStats.node?.name,
- health_status: transformStats.health.status,
- ...(transformStats.health.issues
+ health_status: mapEsHealthStatus2TransformHealthStatus(transformStats.health?.status),
+ ...(transformStats.health?.issues
? {
issues: transformStats.health.issues.map((issue) => {
return {
@@ -238,7 +240,11 @@ export function transformHealthServiceProvider({
const transformsStats = await getTransformStats(transformIds);
return transformsStats
- .filter((t) => t.health.status !== 'green')
+ .filter(
+ (t) =>
+ mapEsHealthStatus2TransformHealthStatus(t.health?.status) !==
+ TRANSFORM_HEALTH_STATUS.green
+ )
.map(baseTransformAlertResponseFormatter);
},
/**
diff --git a/x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts b/x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts
index 78628a8933bc8..618af9ef4e3ef 100644
--- a/x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts
+++ b/x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts
@@ -72,13 +72,13 @@ export default ({ getService }: FtrProviderContext) => {
TRANSFORM_STATE.STOPPED,
`Expected transform state of ${transformId} to be '${TRANSFORM_STATE.STOPPED}' (got ${stats.state})`
);
- expect(stats.health.status).to.eql(
+ expect(stats.health?.status).to.eql(
'red',
- `Expected transform health status of ${transformId} to be 'red' (got ${stats.health.status})`
+ `Expected transform health status of ${transformId} to be 'red' (got ${stats.health?.status})`
);
- expect(stats.health.issues![0].type).to.eql(
+ expect(stats.health?.issues![0].type).to.eql(
'privileges_check_failed',
- `Expected transform health issue of ${transformId} to be 'privileges_check_failed' (got ${stats.health.status})`
+ `Expected transform health issue of ${transformId} to be 'privileges_check_failed' (got ${stats.health?.status})`
);
}
@@ -94,9 +94,9 @@ export default ({ getService }: FtrProviderContext) => {
)})`
);
const stats = await transform.api.getTransformStats(transformId);
- expect(stats.health.status).to.eql(
+ expect(stats.health?.status).to.eql(
'green',
- `Expected transform health status of ${transformId} to be 'green' (got ${stats.health.status})`
+ `Expected transform health status of ${transformId} to be 'green' (got ${stats.health?.status})`
);
}
diff --git a/x-pack/test/functional/apps/transform/actions/starting.ts b/x-pack/test/functional/apps/transform/actions/starting.ts
index 2ecd345ac0e1a..ea12af4f234c9 100644
--- a/x-pack/test/functional/apps/transform/actions/starting.ts
+++ b/x-pack/test/functional/apps/transform/actions/starting.ts
@@ -7,7 +7,7 @@
import {
TRANSFORM_STATE,
- TRANSFORM_HEALTH,
+ TRANSFORM_HEALTH_STATUS,
TRANSFORM_HEALTH_LABEL,
TRANSFORM_HEALTH_DESCRIPTION,
} from '@kbn/transform-plugin/common/constants';
@@ -59,7 +59,7 @@ export default function ({ getService }: FtrProviderContext) {
expected: {
healthDescription: TRANSFORM_HEALTH_DESCRIPTION.green,
healthLabel: TRANSFORM_HEALTH_LABEL.green,
- healthStatus: TRANSFORM_HEALTH.green,
+ healthStatus: TRANSFORM_HEALTH_STATUS.green,
},
},
{
@@ -70,7 +70,7 @@ export default function ({ getService }: FtrProviderContext) {
expected: {
healthDescription: TRANSFORM_HEALTH_DESCRIPTION.green,
healthLabel: TRANSFORM_HEALTH_LABEL.green,
- healthStatus: TRANSFORM_HEALTH.green,
+ healthStatus: TRANSFORM_HEALTH_STATUS.green,
},
},
{
@@ -81,7 +81,7 @@ export default function ({ getService }: FtrProviderContext) {
expected: {
healthDescription: TRANSFORM_HEALTH_DESCRIPTION.yellow,
healthLabel: TRANSFORM_HEALTH_LABEL.yellow,
- healthStatus: TRANSFORM_HEALTH.yellow,
+ healthStatus: TRANSFORM_HEALTH_STATUS.yellow,
},
},
{
@@ -92,7 +92,7 @@ export default function ({ getService }: FtrProviderContext) {
expected: {
healthDescription: TRANSFORM_HEALTH_DESCRIPTION.green,
healthLabel: TRANSFORM_HEALTH_LABEL.green,
- healthStatus: TRANSFORM_HEALTH.green,
+ healthStatus: TRANSFORM_HEALTH_STATUS.green,
},
},
{
@@ -103,7 +103,7 @@ export default function ({ getService }: FtrProviderContext) {
expected: {
healthDescription: TRANSFORM_HEALTH_DESCRIPTION.green,
healthLabel: TRANSFORM_HEALTH_LABEL.green,
- healthStatus: TRANSFORM_HEALTH.green,
+ healthStatus: TRANSFORM_HEALTH_STATUS.green,
},
},
];
@@ -114,7 +114,7 @@ export default function ({ getService }: FtrProviderContext) {
for (const testData of testDataList) {
if (
- testData.expected.healthStatus === TRANSFORM_HEALTH.yellow &&
+ testData.expected.healthStatus === TRANSFORM_HEALTH_STATUS.yellow &&
testData.type === 'pivot'
) {
testData.originalConfig.pivot.aggregations['products.base_price.fail'] = {
@@ -126,7 +126,7 @@ export default function ({ getService }: FtrProviderContext) {
};
}
await transform.api.createTransform(testData.originalConfig.id, testData.originalConfig, {
- deferValidation: testData.expected.healthStatus === TRANSFORM_HEALTH.yellow,
+ deferValidation: testData.expected.healthStatus === TRANSFORM_HEALTH_STATUS.yellow,
});
}
await transform.testResources.setKibanaTimeZoneToUTC();
@@ -167,7 +167,7 @@ export default function ({ getService }: FtrProviderContext) {
await transform.table.assertTransformExpandedRowHealth(
testData.expected.healthDescription,
- testData.expected.healthStatus !== TRANSFORM_HEALTH.green
+ testData.expected.healthStatus !== TRANSFORM_HEALTH_STATUS.green
);
await transform.table.clearSearchString(testDataList.length);
From 53af24e2aea77f29e95473495772efda150e8724 Mon Sep 17 00:00:00 2001
From: Nick Partridge
Date: Wed, 10 Apr 2024 09:20:57 -0700
Subject: [PATCH 24/55] Refactor usage of sparkline chart styles (#179503)
---
src/plugins/charts/public/mocks.ts | 2 +
.../charts/public/services/theme/mock.ts | 20 ++++++++-
.../charts/public/services/theme/theme.ts | 30 ++++++++++++-
.../infra/public/hooks/use_chart_themes.ts | 17 ++++++++
.../single_metric_sparkline.tsx | 19 ++------
.../public/hooks/use_chart_themes.ts | 17 ++++++++
.../metrics/metric_with_sparkline.tsx | 23 +++-------
.../public/pages/overview/overview.tsx | 6 ---
.../pages/rule_details/rule_details.tsx | 7 ---
.../alerts/components/slo_alerts_summary.tsx | 6 ---
.../pages/slos/components/slo_sparkline.tsx | 4 +-
.../.storybook/decorator.tsx | 22 ++++++++++
.../mock/alert_summary_widget/index.ts | 5 +--
.../alert_summary_widget.test.tsx | 13 ++++--
.../alert_summary_widget.tsx | 11 ++++-
.../alert_summary_widget_compact.test.tsx | 8 ++++
.../alert_summary_widget_compact.tsx | 43 +++++++++++--------
.../alert_summary_widget_full_size.test.tsx | 8 ++++
.../alert_summary_widget_full_size.tsx | 29 +++++++------
.../sections/alert_summary_widget/types.ts | 20 ++++++---
.../public/common/get_rule_alerts_summary.tsx | 10 +++--
.../triggers_actions_ui/public/mocks.ts | 7 ++-
.../triggers_actions_ui/public/plugin.ts | 6 ++-
23 files changed, 228 insertions(+), 105 deletions(-)
create mode 100644 x-pack/plugins/observability_solution/infra/public/hooks/use_chart_themes.ts
create mode 100644 x-pack/plugins/observability_solution/observability/public/hooks/use_chart_themes.ts
diff --git a/src/plugins/charts/public/mocks.ts b/src/plugins/charts/public/mocks.ts
index 720202f081ef0..d1eba6f1956a5 100644
--- a/src/plugins/charts/public/mocks.ts
+++ b/src/plugins/charts/public/mocks.ts
@@ -12,6 +12,8 @@ import { activeCursorMock } from './services/active_cursor/mock';
import { colorsServiceMock } from './services/legacy_colors/mock';
import { getPaletteRegistry, paletteServiceMock } from './services/palettes/mock';
+export { MOCK_SPARKLINE_THEME } from './services/theme/mock';
+
export type Setup = jest.Mocked>;
export type Start = jest.Mocked>;
diff --git a/src/plugins/charts/public/services/theme/mock.ts b/src/plugins/charts/public/services/theme/mock.ts
index d41f9f20dd382..ce21174a9a5a6 100644
--- a/src/plugins/charts/public/services/theme/mock.ts
+++ b/src/plugins/charts/public/services/theme/mock.ts
@@ -6,9 +6,26 @@
* Side Public License, v 1.
*/
-import { LIGHT_THEME } from '@elastic/charts';
+import { LIGHT_THEME, PartialTheme } from '@elastic/charts';
import { ThemeService } from './theme';
+export const MOCK_SPARKLINE_THEME: PartialTheme = {
+ lineSeriesStyle: {
+ point: {
+ visible: false,
+ strokeWidth: 1,
+ radius: 1,
+ },
+ },
+ areaSeriesStyle: {
+ point: {
+ visible: false,
+ strokeWidth: 1,
+ radius: 1,
+ },
+ },
+};
+
export const themeServiceMock: ThemeService = {
chartsDefaultBaseTheme: LIGHT_THEME,
chartsBaseTheme$: jest.fn(() => ({
@@ -19,4 +36,5 @@ export const themeServiceMock: ThemeService = {
})),
useDarkMode: jest.fn().mockReturnValue(false),
useChartsBaseTheme: jest.fn().mockReturnValue(LIGHT_THEME),
+ useSparklineOverrides: jest.fn().mockReturnValue(MOCK_SPARKLINE_THEME),
} as any;
diff --git a/src/plugins/charts/public/services/theme/theme.ts b/src/plugins/charts/public/services/theme/theme.ts
index b8ee2301dbb82..c3ee3f8b8c14b 100644
--- a/src/plugins/charts/public/services/theme/theme.ts
+++ b/src/plugins/charts/public/services/theme/theme.ts
@@ -52,6 +52,30 @@ export class ThemeService {
return {};
};
+ /**
+ * A react hook to return shared sparkline chart overrides
+ *
+ * Replacement for `EUI_SPARKLINE_THEME_PARTIAL`
+ */
+ public useSparklineOverrides = (): PartialTheme => {
+ return {
+ lineSeriesStyle: {
+ point: {
+ visible: false,
+ strokeWidth: 1,
+ radius: 1,
+ },
+ },
+ areaSeriesStyle: {
+ point: {
+ visible: false,
+ strokeWidth: 1,
+ radius: 1,
+ },
+ },
+ };
+ };
+
/** A React hook for consuming the charts theme */
public useChartsBaseTheme = (): Theme => {
const [value, update] = useState(this._chartsBaseTheme$.getValue());
@@ -70,7 +94,11 @@ export class ThemeService {
return value;
};
- /** initialize service with uiSettings */
+ /**
+ * Initialize theme service with dark mode
+ *
+ * Meant to be called by charts plugin setup method
+ */
public init(theme: CoreSetup['theme']) {
this.theme$ = theme.theme$;
this.theme$.subscribe(({ darkMode }) => {
diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_chart_themes.ts b/x-pack/plugins/observability_solution/infra/public/hooks/use_chart_themes.ts
new file mode 100644
index 0000000000000..07c085bcc35be
--- /dev/null
+++ b/x-pack/plugins/observability_solution/infra/public/hooks/use_chart_themes.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 { useKibanaContextForPlugin } from './use_kibana';
+
+export const useChartThemes = () => {
+ const { services } = useKibanaContextForPlugin();
+
+ return {
+ baseTheme: services.charts.theme.useChartsBaseTheme(),
+ sparklineTheme: services.charts.theme.useSparklineOverrides(),
+ };
+};
diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx
index d51b1ff370fe3..5c38c0055cf38 100644
--- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/sections/top_categories/single_metric_sparkline.tsx
@@ -6,21 +6,11 @@
*/
import React, { useMemo } from 'react';
-import {
- Chart,
- Settings,
- AreaSeries,
- ScaleType,
- TooltipType,
- Tooltip,
- LIGHT_THEME,
- DARK_THEME,
-} from '@elastic/charts';
-import { EUI_SPARKLINE_THEME_PARTIAL } from '@elastic/eui/dist/eui_charts_theme';
+import { Chart, Settings, AreaSeries, ScaleType, TooltipType, Tooltip } from '@elastic/charts';
import { i18n } from '@kbn/i18n';
-import { useIsDarkMode } from '../../../../../hooks/use_is_dark_mode';
import { useKibanaTimeZoneSetting } from '../../../../../hooks/use_kibana_time_zone_setting';
import { TimeRange } from '../../../../../../common/time';
+import { useChartThemes } from '../../../../../hooks/use_chart_themes';
interface TimeSeriesPoint {
timestamp: number;
@@ -38,9 +28,8 @@ export const SingleMetricSparkline: React.FunctionComponent<{
metric: TimeSeriesPoint[];
timeRange: TimeRange;
}> = ({ metric, timeRange }) => {
- const isDarkMode = useIsDarkMode();
const timeZone = useKibanaTimeZoneSetting();
- const baseTheme = useMemo(() => (isDarkMode ? DARK_THEME : LIGHT_THEME), [isDarkMode]);
+ const { baseTheme, sparklineTheme } = useChartThemes();
const xDomain = useMemo(
() => ({
@@ -55,7 +44,7 @@ export const SingleMetricSparkline: React.FunctionComponent<{
{
+ const { charts } = useKibana().services;
+
+ return {
+ baseTheme: charts.theme.useChartsBaseTheme(),
+ sparklineTheme: charts.theme.useSparklineOverrides(),
+ };
+};
diff --git a/x-pack/plugins/observability_solution/observability/public/pages/overview/components/sections/metrics/metric_with_sparkline.tsx b/x-pack/plugins/observability_solution/observability/public/pages/overview/components/sections/metrics/metric_with_sparkline.tsx
index bd292a2bf88cb..edd56b2c9b640 100644
--- a/x-pack/plugins/observability_solution/observability/public/pages/overview/components/sections/metrics/metric_with_sparkline.tsx
+++ b/x-pack/plugins/observability_solution/observability/public/pages/overview/components/sections/metrics/metric_with_sparkline.tsx
@@ -5,23 +5,14 @@
* 2.0.
*/
-import {
- Chart,
- Settings,
- AreaSeries,
- TooltipType,
- Tooltip,
- LIGHT_THEME,
- DARK_THEME,
-} from '@elastic/charts';
+import { Chart, Settings, AreaSeries, TooltipType, Tooltip } from '@elastic/charts';
import { EuiFlexItem, EuiFlexGroup, EuiIcon, EuiTextColor } from '@elastic/eui';
-import React, { useContext } from 'react';
+import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
-import { EUI_SPARKLINE_THEME_PARTIAL } from '@elastic/eui/dist/eui_charts_theme';
-import { ThemeContext } from 'styled-components';
import { i18n } from '@kbn/i18n';
import { NumberOrNull } from '../../../../..';
+import { useChartThemes } from '../../../../../hooks/use_chart_themes';
interface Props {
id: string;
@@ -31,11 +22,7 @@ interface Props {
color: number;
}
export function MetricWithSparkline({ id, formatter, value, timeseries, color }: Props) {
- const themeCTX = useContext(ThemeContext);
- const isDarkTheme = (themeCTX && themeCTX.darkMode) || false;
- const theme = [EUI_SPARKLINE_THEME_PARTIAL];
- const baseTheme = isDarkTheme ? DARK_THEME : LIGHT_THEME;
-
+ const { baseTheme, sparklineTheme } = useChartThemes();
const colors = baseTheme.colors?.vizColors ?? [];
if (!value) {
@@ -57,7 +44,7 @@ export function MetricWithSparkline({ id, formatter, value, timeseries, color }:
diff --git a/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx b/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx
index 5cc5e2ac967ea..2156a3d0cbc43 100644
--- a/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx
+++ b/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx
@@ -39,7 +39,6 @@ const ALERTS_TABLE_ID = 'xpack.observability.overview.alert.table';
export function OverviewPage() {
const {
- charts,
http,
triggersActionsUi: {
alertsTableConfigurationRegistry,
@@ -103,10 +102,6 @@ export function OverviewPage() {
[bucketSize, relativeEnd, relativeStart]
);
- const chartProps = {
- baseTheme: charts.theme.useChartsBaseTheme(),
- };
-
useEffect(() => {
setEsQuery(
buildEsQuery({
@@ -184,7 +179,6 @@ export function OverviewPage() {
hasError={false}
>
();
const { search } = useLocation();
-
- const baseTheme = useChartsBaseTheme();
-
const { rule, isLoading, isError, refetch } = useFetchRule({ ruleId });
const filteredRuleTypes = useGetFilteredRuleTypes();
const { ruleTypes } = useFetchRuleTypes({
@@ -233,7 +227,6 @@ export function RuleDetailsPage() {
{
if (onLoaded) {
diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_sparkline.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_sparkline.tsx
index 0bb96b39d0649..e220643bbc3b3 100644
--- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_sparkline.tsx
+++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_sparkline.tsx
@@ -18,7 +18,6 @@ import {
} from '@elastic/charts';
import React from 'react';
import { EuiLoadingChart, useEuiTheme } from '@elastic/eui';
-import { EUI_SPARKLINE_THEME_PARTIAL } from '@elastic/eui/dist/eui_charts_theme';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../../utils/kibana_react';
@@ -42,6 +41,7 @@ export interface Props {
export function SloSparkline({ chart, data, id, isLoading, size, state }: Props) {
const charts = useKibana().services.charts;
const baseTheme = charts.theme.useChartsBaseTheme();
+ const sparklineTheme = charts.theme.useSparklineOverrides();
const { euiTheme } = useEuiTheme();
@@ -60,7 +60,7 @@ export function SloSparkline({ chart, data, id, isLoading, size, state }: Props)
diff --git a/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx b/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx
index 7b22d0520a6c4..cbfc5038446f5 100644
--- a/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx
+++ b/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx
@@ -15,6 +15,7 @@ import { KibanaThemeProvider, KibanaServices } from '@kbn/kibana-react-plugin/pu
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import type { NotificationsStart, ApplicationStart } from '@kbn/core/public';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { DARK_THEME, LIGHT_THEME } from '@elastic/charts';
import { KibanaContextProvider } from '../public/common/lib/kibana';
import { ExperimentalFeaturesService } from '../public/common/experimental_features_service';
import { getHttp } from './context/http';
@@ -92,6 +93,27 @@ export const StorybookContextDecorator: React.FC
http: getHttp(context),
actionTypeRegistry: getActionTypeRegistry(),
ruleTypeRegistry: getRuleTypeRegistry(),
+ charts: {
+ theme: {
+ useChartsBaseTheme: () => (darkMode ? DARK_THEME : LIGHT_THEME),
+ useSparklineOverrides: () => ({
+ lineSeriesStyle: {
+ point: {
+ visible: false,
+ strokeWidth: 1,
+ radius: 1,
+ },
+ },
+ areaSeriesStyle: {
+ point: {
+ visible: false,
+ strokeWidth: 1,
+ radius: 1,
+ },
+ },
+ }),
+ },
+ },
...servicesOverride,
}}
>
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/mock/alert_summary_widget/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/mock/alert_summary_widget/index.ts
index bb07375721059..eae946f4eda41 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/mock/alert_summary_widget/index.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/mock/alert_summary_widget/index.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { LIGHT_THEME } from '@elastic/charts';
import { AlertSummaryTimeRange, ChartProps } from '../../sections/alert_summary_widget/types';
export const mockedAlertSummaryResponse = {
@@ -38,6 +37,4 @@ export const mockedAlertSummaryTimeRange: AlertSummaryTimeRange = {
title: 'mockedTitle',
};
-export const mockedChartProps: ChartProps = {
- baseTheme: LIGHT_THEME,
-};
+export const mockedChartProps: ChartProps = {};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.test.tsx
index a04fe6ed70eb8..bf5f487cfafa9 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.test.tsx
@@ -9,20 +9,19 @@ import React from 'react';
import { render } from '@testing-library/react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { AlertSummaryWidget } from './alert_summary_widget';
-import { AlertSummaryWidgetProps } from './types';
+import { AlertSummaryWidgetDependencies, AlertSummaryWidgetProps } from './types';
import { mockedAlertSummaryTimeRange, mockedChartProps } from '../../mock/alert_summary_widget';
import { useLoadAlertSummary } from '../../hooks/use_load_alert_summary';
import {
ACTIVE_ALERT_COUNT_DATA_TEST_SUBJ,
TOTAL_ALERT_COUNT_DATA_TEST_SUBJ,
} from './components/constants';
+import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
useUiSetting: jest.fn().mockImplementation(() => true),
}));
-const TITLE_DATA_TEST_SUBJ = 'mockedTimeRangeTitle';
-
jest.mock('../../hooks/use_load_alert_summary', () => ({
useLoadAlertSummary: jest.fn().mockReturnValue({
alertSummary: {
@@ -35,8 +34,15 @@ jest.mock('../../hooks/use_load_alert_summary', () => ({
},
}),
}));
+
+const TITLE_DATA_TEST_SUBJ = 'mockedTimeRangeTitle';
+
const useLoadAlertSummaryMock = useLoadAlertSummary as jest.Mock;
+const dependencies: AlertSummaryWidgetDependencies['dependencies'] = {
+ charts: chartPluginMock.createStartContract(),
+};
+
describe('AlertSummaryWidget', () => {
const mockedTimeRange = {
...mockedAlertSummaryTimeRange,
@@ -51,6 +57,7 @@ describe('AlertSummaryWidget', () => {
featureIds={['apm', 'uptime', 'logs']}
onClick={jest.fn}
timeRange={mockedTimeRange}
+ dependencies={dependencies}
{...props}
/>
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx
index 2619ef2b25258..8702cc50910c8 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx
@@ -14,6 +14,7 @@ import {
AlertSummaryWidgetFullSize,
AlertSummaryWidgetLoader,
} from './components';
+import { AlertSummaryWidgetDependencies, DependencyProps } from './types';
export const AlertSummaryWidget = ({
chartProps,
@@ -24,7 +25,8 @@ export const AlertSummaryWidget = ({
timeRange,
hideChart,
onLoaded,
-}: AlertSummaryWidgetProps) => {
+ dependencies: { charts },
+}: AlertSummaryWidgetProps & AlertSummaryWidgetDependencies) => {
const {
alertSummary: { activeAlertCount, activeAlerts, recoveredAlertCount },
isLoading,
@@ -41,6 +43,11 @@ export const AlertSummaryWidget = ({
}
}, [activeAlertCount, isLoading, onLoaded, recoveredAlertCount]);
+ const dependencyProps: DependencyProps = {
+ baseTheme: charts.theme.useChartsBaseTheme(),
+ sparklineTheme: charts.theme.useSparklineOverrides(),
+ };
+
if (isLoading)
return ;
@@ -56,6 +63,7 @@ export const AlertSummaryWidget = ({
dateFormat={timeRange.dateFormat}
recoveredAlertCount={recoveredAlertCount}
hideChart={hideChart}
+ dependencyProps={dependencyProps}
/>
) : null
) : (
@@ -66,6 +74,7 @@ export const AlertSummaryWidget = ({
onClick={onClick}
recoveredAlertCount={recoveredAlertCount}
timeRangeTitle={timeRange.title}
+ dependencyProps={dependencyProps}
/>
);
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_compact.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_compact.test.tsx
index 58abc8299677b..b0f826ae2eb65 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_compact.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_compact.test.tsx
@@ -14,6 +14,13 @@ import {
import { render } from '@testing-library/react';
import { mockedAlertSummaryResponse, mockedChartProps } from '../../../mock/alert_summary_widget';
import { ACTIVE_ALERT_COUNT_DATA_TEST_SUBJ, TOTAL_ALERT_COUNT_DATA_TEST_SUBJ } from './constants';
+import { LIGHT_THEME } from '@elastic/charts';
+import { DependencyProps } from '../types';
+
+const dependencyProps: DependencyProps = {
+ baseTheme: LIGHT_THEME,
+ sparklineTheme: {},
+};
describe('AlertSummaryWidgetCompact', () => {
const renderComponent = (props: Partial = {}) =>
@@ -22,6 +29,7 @@ describe('AlertSummaryWidgetCompact', () => {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_compact.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_compact.tsx
index 2f53ada96bca6..3fd94f3b5a2c8 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_compact.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_compact.tsx
@@ -18,43 +18,31 @@ import {
TooltipType,
Tooltip,
} from '@elastic/charts';
-import { EUI_SPARKLINE_THEME_PARTIAL } from '@elastic/eui/dist/eui_charts_theme';
import { AlertStatus } from '@kbn/rule-data-utils';
import { i18n } from '@kbn/i18n';
import { AlertCounts } from './alert_counts';
import { ALL_ALERT_COLOR, WIDGET_TITLE } from './constants';
-import { Alert, ChartProps } from '../types';
+import { Alert, ChartProps, DependencyProps } from '../types';
export interface AlertSummaryWidgetCompactProps {
activeAlertCount: number;
activeAlerts: Alert[];
- chartProps: ChartProps;
+ chartProps?: ChartProps;
recoveredAlertCount: number;
timeRangeTitle?: JSX.Element | string;
onClick: (status?: AlertStatus) => void;
+ dependencyProps: DependencyProps;
}
export const AlertSummaryWidgetCompact = ({
activeAlertCount,
activeAlerts,
- chartProps: { theme, baseTheme },
+ chartProps: { themeOverrides } = {},
recoveredAlertCount,
timeRangeTitle,
onClick,
+ dependencyProps: { baseTheme, sparklineTheme },
}: AlertSummaryWidgetCompactProps) => {
- const chartTheme = [
- ...(theme ? [theme] : []),
- EUI_SPARKLINE_THEME_PARTIAL,
- {
- chartMargins: {
- left: 10,
- right: 10,
- top: 10,
- bottom: 10,
- },
- },
- ];
-
const handleClick = (
event: MouseEvent,
status?: AlertStatus
@@ -99,7 +87,26 @@ export const AlertSummaryWidgetCompact = ({
-
+
{
const renderComponent = (props: Partial = {}) =>
@@ -21,6 +28,7 @@ describe('AlertSummaryWidgetFullSize', () => {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_full_size.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_full_size.tsx
index 5eae23e396721..f2ebfd987b308 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_full_size.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/components/alert_summary_widget_full_size.tsx
@@ -21,33 +21,27 @@ import { EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AlertCounts } from './alert_counts';
import { ALL_ALERT_COLOR, TOOLTIP_DATE_FORMAT } from './constants';
-import { Alert, ChartProps } from '../types';
+import { Alert, ChartProps, DependencyProps } from '../types';
export interface AlertSummaryWidgetFullSizeProps {
activeAlertCount: number;
activeAlerts: Alert[];
- chartProps: ChartProps;
+ chartProps?: ChartProps;
recoveredAlertCount: number;
dateFormat?: string;
hideChart?: boolean;
+ dependencyProps: DependencyProps;
}
export const AlertSummaryWidgetFullSize = ({
activeAlertCount,
activeAlerts,
- chartProps: { theme, baseTheme, onBrushEnd },
+ chartProps: { themeOverrides, onBrushEnd } = {},
dateFormat,
recoveredAlertCount,
hideChart,
+ dependencyProps: { baseTheme },
}: AlertSummaryWidgetFullSizeProps) => {
- const chartTheme = [
- ...(theme ? [theme] : []),
- {
- chartPaddings: {
- top: 7,
- },
- },
- ];
const chartData = activeAlerts.map((alert) => alert.doc_count);
const domain = {
max: Math.max(...chartData) * 1.1, // add 10% headroom
@@ -78,7 +72,18 @@ export const AlertSummaryWidgetFullSize = ({
/>
void;
timeRange: AlertSummaryTimeRange;
- chartProps: ChartProps;
+ chartProps?: ChartProps;
hideChart?: boolean;
onLoaded?: (alertsCount?: AlertsCount) => void;
}
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_alerts_summary.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_alerts_summary.tsx
index 21763d1e5f0c9..e10f265deac1a 100644
--- a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_alerts_summary.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_alerts_summary.tsx
@@ -8,12 +8,14 @@
import React, { lazy, Suspense } from 'react';
import { AlertSummaryWidgetLoader } from '../application/sections/alert_summary_widget/components';
import { AlertSummaryWidgetProps } from '../application/sections/alert_summary_widget';
+import { AlertSummaryWidgetDependencies } from '../application/sections/alert_summary_widget/types';
-const AlertSummaryWidgetLazy: React.FC = lazy(
- () => import('../application/sections/alert_summary_widget/alert_summary_widget')
-);
+const AlertSummaryWidgetLazy: React.FC =
+ lazy(() => import('../application/sections/alert_summary_widget/alert_summary_widget'));
-export const getAlertSummaryWidgetLazy = (props: AlertSummaryWidgetProps) => {
+export const getAlertSummaryWidgetLazy = (
+ props: AlertSummaryWidgetProps & AlertSummaryWidgetDependencies
+) => {
return (
();
@@ -142,7 +144,10 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart {
});
},
getAlertSummaryWidget: (props) => {
- return getAlertSummaryWidgetLazy(props);
+ const dependencies: AlertSummaryWidgetDependencies['dependencies'] = {
+ charts: chartPluginMock.createStartContract(),
+ };
+ return getAlertSummaryWidgetLazy({ ...props, dependencies });
},
getRuleDefinition: (props) => {
return getRuleDefinitionLazy({ ...props, actionTypeRegistry, ruleTypeRegistry });
diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts
index fa0db949e3b50..218eb01e39d70 100644
--- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts
@@ -97,6 +97,7 @@ import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal';
import { getRulesSettingsLinkLazy } from './common/get_rules_settings_link';
import { getGlobalRuleEventLogListLazy } from './common/get_global_rule_event_log_list';
import { AlertTableConfigRegistry } from './application/alert_table_config_registry';
+import { AlertSummaryWidgetDependencies } from './application/sections/alert_summary_widget/types';
export interface TriggersAndActionsUIPublicPluginSetup {
actionTypeRegistry: TypeRegistry;
@@ -559,7 +560,10 @@ export class Plugin
return getRuleStatusPanelLazy(props);
},
getAlertSummaryWidget: (props: AlertSummaryWidgetProps) => {
- return getAlertSummaryWidgetLazy(props);
+ const dependencies: AlertSummaryWidgetDependencies['dependencies'] = {
+ charts: plugins.charts,
+ };
+ return getAlertSummaryWidgetLazy({ ...props, dependencies });
},
getRuleSnoozeModal: (props: RuleSnoozeModalProps) => {
return getRuleSnoozeModalLazy(props);
From a6a6823d284fc0ca63ef362612750b0d97ebf30d Mon Sep 17 00:00:00 2001
From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com>
Date: Wed, 10 Apr 2024 18:28:58 +0200
Subject: [PATCH 25/55] [Lens] legend popover style changes (#180354)
## Summary
These changes are just small redesign of the Legend popover in
preparation for Legend stats changes (following the design from
https://github.com/elastic/kibana/issues/176583)
I also rewrote the tests to testing library and removed deprecated
`name` prop for EuiButtonGroup that it doesn't need anymore.
Design:
Changes that I could implement in this PR:
for xy:
for pie:
---
.../kbn-test-eui-helpers/src/rtl_helpers.tsx | 2 +-
.../operations/definitions/terms/index.tsx | 1 -
.../axis/extent/axis_extent_settings.tsx | 2 -
.../legend/legend_settings_popover.test.tsx | 157 ++++++++---------
.../legend/legend_settings_popover.tsx | 74 +++++---
.../legend_location_settings.test.tsx | 121 ++++++-------
.../location/legend_location_settings.tsx | 162 +++++++++---------
.../shared_components/toolbar_popover.tsx | 18 +-
.../value_labels_settings.tsx | 1 -
.../datatable/components/dimension_editor.tsx | 2 -
.../datatable/components/toolbar.test.tsx | 6 +-
.../legacy_metric/dimension_editor.tsx | 1 -
.../metric/dimension_editor.tsx | 1 -
.../visualizations/partition/toolbar.tsx | 1 -
.../xy_config_panel/axis_settings_popover.tsx | 1 -
.../xy/xy_config_panel/dimension_editor.tsx | 1 -
.../reference_line_panel.tsx | 1 -
.../shared/marker_decoration_settings.tsx | 1 -
.../translations/translations/fr-FR.json | 6 +-
.../translations/translations/ja-JP.json | 6 +-
.../translations/translations/zh-CN.json | 6 +-
21 files changed, 272 insertions(+), 299 deletions(-)
diff --git a/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx b/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx
index fc469d9cb234f..19faf02262aae 100644
--- a/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx
+++ b/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx
@@ -9,7 +9,7 @@ import moment from 'moment';
import userEvent from '@testing-library/user-event';
import { screen, within, fireEvent } from '@testing-library/react';
-export const getButtonGroupInputValue = (testId: string) => () => {
+export const getSelectedButtonInGroup = (testId: string) => () => {
const buttonGroup = screen.getByTestId(testId);
return within(buttonGroup).getByRole('button', { pressed: true });
};
diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/index.tsx
index 155fe24705276..0ffc918844f75 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/index.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/index.tsx
@@ -960,7 +960,6 @@ The top values of a specified field ranked by the chosen metric.
defaultMessage: 'Rank direction',
})}
data-test-subj="indexPattern-terms-orderDirection-groups"
- name="orderDirection"
buttonSize="compressed"
aria-label={i18n.translate('xpack.lens.indexPattern.terms.orderDirection', {
defaultMessage: 'Rank direction',
diff --git a/x-pack/plugins/lens/public/shared_components/axis/extent/axis_extent_settings.tsx b/x-pack/plugins/lens/public/shared_components/axis/extent/axis_extent_settings.tsx
index a015c0d477dba..66a4326d04198 100644
--- a/x-pack/plugins/lens/public/shared_components/axis/extent/axis_extent_settings.tsx
+++ b/x-pack/plugins/lens/public/shared_components/axis/extent/axis_extent_settings.tsx
@@ -179,7 +179,6 @@ function MetricAxisBoundsControl({
defaultMessage: 'Bounds',
})}
data-test-subj={`${testSubjPrefix}_axisBounds_groups`}
- name="axisBounds"
buttonSize="compressed"
options={[
{
@@ -250,7 +249,6 @@ function BucketAxisBoundsControl({
defaultMessage: 'Bounds',
})}
data-test-subj={`${testSubjPrefix}_axisBounds_groups`}
- name="axisBounds"
buttonSize="compressed"
options={[
{
diff --git a/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.test.tsx b/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.test.tsx
index 777f2860cb8b6..5350f1468c27c 100644
--- a/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.test.tsx
+++ b/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.test.tsx
@@ -6,35 +6,32 @@
*/
import React from 'react';
-import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers';
-import {
- LegendSettingsPopover,
- LegendSettingsPopoverProps,
- MaxLinesInput,
-} from './legend_settings_popover';
+import { LegendSettingsPopover, LegendSettingsPopoverProps } from './legend_settings_popover';
+import userEvent from '@testing-library/user-event';
+import { RenderOptions, fireEvent, render, screen } from '@testing-library/react';
+import { getSelectedButtonInGroup } from '@kbn/test-eui-helpers';
describe('Legend Settings', () => {
- const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: string }> = [
- {
- id: `test_legend_auto`,
- value: 'auto',
- label: 'Auto',
- },
- {
- id: `test_legend_show`,
- value: 'show',
- label: 'Show',
- },
- {
- id: `test_legend_hide`,
- value: 'hide',
- label: 'Hide',
- },
- ];
- let props: LegendSettingsPopoverProps;
+ let defaultProps: LegendSettingsPopoverProps;
beforeEach(() => {
- props = {
- legendOptions,
+ defaultProps = {
+ legendOptions: [
+ {
+ id: `test_legend_auto`,
+ value: 'auto',
+ label: 'Auto',
+ },
+ {
+ id: `test_legend_show`,
+ value: 'show',
+ label: 'Show',
+ },
+ {
+ id: `test_legend_hide`,
+ value: 'hide',
+ label: 'Hide',
+ },
+ ],
mode: 'auto',
showAutoLegendSizeOption: true,
onDisplayChange: jest.fn(),
@@ -43,83 +40,89 @@ describe('Legend Settings', () => {
};
});
- it('should have selected the given mode as Display value', () => {
- const component = shallow( );
- expect(component.find('[data-test-subj="lens-legend-display-btn"]').prop('idSelected')).toEqual(
- 'test_legend_auto'
+ const renderLegendSettingsPopover = (
+ overrideProps?: Partial,
+ renderOptions?: RenderOptions
+ ) => {
+ const rtlRender = render(
+ ,
+ renderOptions
);
+ const openLegendPopover = () => userEvent.click(screen.getByRole('button', { name: 'Legend' }));
+
+ openLegendPopover();
+
+ return {
+ ...rtlRender,
+ getSelectedDisplayOption: getSelectedButtonInGroup('lens-legend-display-btn'),
+ };
+ };
+
+ it('should have selected the given mode as Display value', () => {
+ const { getSelectedDisplayOption } = renderLegendSettingsPopover();
+ expect(getSelectedDisplayOption()).toHaveTextContent('Auto');
});
it('should have called the onDisplayChange function on ButtonGroup change', () => {
- const component = shallow( );
- component.find('[data-test-subj="lens-legend-display-btn"]').simulate('change');
- expect(props.onDisplayChange).toHaveBeenCalled();
+ renderLegendSettingsPopover();
+ fireEvent.click(screen.getByRole('button', { name: 'Show' }));
+ expect(defaultProps.onDisplayChange).toHaveBeenCalled();
});
- it('should have default the max lines input to 1 when no value is given', () => {
- const component = shallow( );
- expect(component.find(MaxLinesInput).prop('value')).toEqual(1);
+ it('should have default line limit set to one and be enabled when it is on', () => {
+ renderLegendSettingsPopover({ shouldTruncate: true });
+ const lineLimit = screen.getByRole('spinbutton', { name: 'Line limit' });
+ expect(lineLimit).toHaveValue(1);
+ expect(lineLimit).not.toBeDisabled();
});
- it('should have the `Truncate legend text` switch enabled by default', () => {
- const component = shallow( );
- expect(
- component.find('[data-test-subj="lens-legend-truncate-switch"]').prop('checked')
- ).toEqual(true);
+ it('should have default line limit set to one and be disabled when it is off', () => {
+ renderLegendSettingsPopover({ shouldTruncate: false });
+ const lineLimit = screen.getByRole('spinbutton', { name: 'Line limit' });
+ expect(lineLimit).toHaveValue(1);
+ expect(lineLimit).toBeDisabled();
});
- it('should set the truncate switch state when truncate prop value is false', () => {
- const component = shallow( );
- expect(
- component.find('[data-test-subj="lens-legend-truncate-switch"]').prop('checked')
- ).toEqual(false);
+ it('should have the `Label truncation` switch enabled by default', () => {
+ renderLegendSettingsPopover();
+ const switchElement = screen.getByRole('switch', { name: 'Label truncation' });
+ expect(switchElement).toBeChecked();
});
- it('should hide the max lines input when truncate is set to false', () => {
- const component = shallow( );
- expect(component.exists(MaxLinesInput)).toEqual(false);
+ it('should set the truncate switch state when truncate prop value is false', () => {
+ renderLegendSettingsPopover({ shouldTruncate: false });
+ const switchElement = screen.getByRole('switch', { name: 'Label truncation' });
+ expect(switchElement).not.toBeChecked();
});
it('should have called the onTruncateLegendChange function on truncate switch change', () => {
- const nestedProps = {
- ...props,
- shouldTruncate: true,
- onTruncateLegendChange: jest.fn(),
- };
- const component = shallow( );
- component.find('[data-test-subj="lens-legend-truncate-switch"]').simulate('change');
- expect(nestedProps.onTruncateLegendChange).toHaveBeenCalled();
+ const onTruncateLegendChange = jest.fn();
+ renderLegendSettingsPopover({ onTruncateLegendChange });
+ const switchElement = screen.getByRole('switch', { name: 'Label truncation' });
+ fireEvent.click(switchElement);
+ expect(onTruncateLegendChange).toHaveBeenCalled();
});
it('should enable the Nested Legend Switch when renderNestedLegendSwitch prop is true', () => {
- const component = shallow( );
- expect(component.find('[data-test-subj="lens-legend-nested-switch"]')).toHaveLength(1);
+ renderLegendSettingsPopover({ renderNestedLegendSwitch: true });
+ expect(screen.getByRole('switch', { name: 'Nested' })).toBeEnabled();
});
it('should set the switch state on nestedLegend prop value', () => {
- const component = shallow(
-
- );
- expect(component.find('[data-test-subj="lens-legend-nested-switch"]').prop('checked')).toEqual(
- true
- );
+ renderLegendSettingsPopover({ renderNestedLegendSwitch: true, nestedLegend: true });
+ expect(screen.getByRole('switch', { name: 'Nested' })).toBeChecked();
});
it('should have called the onNestedLegendChange function on switch change', () => {
- const nestedProps = {
- ...props,
- renderNestedLegendSwitch: true,
- onNestedLegendChange: jest.fn(),
- };
- const component = shallow( );
- component.find('[data-test-subj="lens-legend-nested-switch"]').simulate('change');
- expect(nestedProps.onNestedLegendChange).toHaveBeenCalled();
+ const onNestedLegendChange = jest.fn();
+ renderLegendSettingsPopover({ renderNestedLegendSwitch: true, onNestedLegendChange });
+ const switchElement = screen.getByRole('switch', { name: 'Nested' });
+ fireEvent.click(switchElement);
+ expect(onNestedLegendChange).toHaveBeenCalled();
});
it('should hide switch group on hide mode', () => {
- const component = shallow(
-
- );
- expect(component.exists('[data-test-subj="lens-legend-nested-switch"]')).toEqual(false);
+ renderLegendSettingsPopover({ mode: 'hide', renderNestedLegendSwitch: true });
+ expect(screen.queryByRole('switch', { name: 'Nested' })).toBeNull();
});
});
diff --git a/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.tsx
index 64a374bae7052..0add0332d65b6 100644
--- a/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.tsx
+++ b/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.tsx
@@ -13,6 +13,8 @@ import {
EuiSwitch,
EuiSwitchEvent,
EuiFieldNumber,
+ EuiFlexItem,
+ EuiFlexGroup,
} from '@elastic/eui';
import { Position, VerticalAlignment, HorizontalAlignment } from '@elastic/charts';
import { LegendSize } from '@kbn/visualizations-plugin/public';
@@ -141,13 +143,23 @@ const MIN_TRUNCATE_LINES = 1;
export const MaxLinesInput = ({
value,
setValue,
+ disabled,
}: {
value: number;
setValue: (value: number) => void;
+ disabled?: boolean;
}) => {
const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange: setValue });
return (
{};
+const PANEL_STYLE = {
+ width: '500px',
+};
export const LegendSettingsPopover: React.FunctionComponent = ({
legendOptions,
@@ -202,12 +217,14 @@ export const LegendSettingsPopover: React.FunctionComponent
value === mode)!.id}
@@ -250,43 +266,44 @@ export const LegendSettingsPopover: React.FunctionComponent
)}
+
-
+
+
+
+
+
+
+
+
- {shouldTruncate && (
-
-
-
- )}
+
{renderNestedLegendSwitch && (
{
- let props: LegendLocationSettingsProps;
+ let defaultProps: LegendLocationSettingsProps;
beforeEach(() => {
- props = {
+ defaultProps = {
onLocationChange: jest.fn(),
onPositionChange: jest.fn(),
+ onAlignmentChange: jest.fn(),
+ location: 'outside',
};
});
- it('should have default the Position to right when no position is given', () => {
- const component = shallow( );
- expect(
- component.find('[data-test-subj="lens-legend-position-btn"]').prop('idSelected')
- ).toEqual(Position.Right);
+ afterEach(() => {
+ jest.clearAllMocks();
});
- it('should have called the onPositionChange function on ButtonGroup change', () => {
- const component = shallow( );
- component.find('[data-test-subj="lens-legend-position-btn"]').simulate('change');
- expect(props.onPositionChange).toHaveBeenCalled();
+ const renderLegendLocationSettings = (
+ overrideProps?: Partial,
+ renderOptions?: RenderOptions
+ ) => {
+ const rtlRender = render(
+ ,
+ renderOptions
+ );
+
+ return {
+ ...rtlRender,
+ getSelectedPositionOption: getSelectedButtonInGroup('lens-legend-position-btn'),
+ getSelectedLocationOption: getSelectedButtonInGroup('lens-legend-location-btn'),
+ getSelectedAlignmentOption: getSelectedButtonInGroup('lens-legend-inside-alignment-btn'),
+ };
+ };
+
+ it('should have default the Position to right when no position is given', () => {
+ const { getSelectedPositionOption } = renderLegendLocationSettings();
+ expect(getSelectedPositionOption()).toHaveTextContent('Right');
});
- it('should hide the position group if isDisabled prop is true', () => {
- const component = shallow( );
- expect(component.exists('[data-test-subj="lens-legend-position-btn"]')).toEqual(false);
+ it('should have called the onPositionChange function on ButtonGroup change', () => {
+ renderLegendLocationSettings();
+ fireEvent.click(screen.getByRole('button', { name: 'Left' }));
+ expect(defaultProps.onPositionChange).toHaveBeenCalled();
});
it('should hide the position button group if location inside is given', () => {
- const newProps = {
- ...props,
- location: 'inside',
- } as LegendLocationSettingsProps;
- const component = shallow( );
- expect(component.find('[data-test-subj="lens-legend-position-btn"]').length).toEqual(0);
+ renderLegendLocationSettings({ location: 'inside' });
+ expect(screen.queryByTestId('lens-legend-position-btn')).toBeNull();
});
it('should render the location settings if location inside is given', () => {
- const newProps = {
- ...props,
- location: 'inside',
- } as LegendLocationSettingsProps;
- const component = shallow( );
- expect(component.find('[data-test-subj="lens-legend-location-btn"]').length).toEqual(1);
+ renderLegendLocationSettings({ location: 'inside' });
+ expect(screen.queryByTestId('lens-legend-location-btn')).toBeInTheDocument();
});
it('should have selected the given location', () => {
- const newProps = {
- ...props,
- location: 'inside',
- } as LegendLocationSettingsProps;
- const component = shallow( );
- expect(
- component.find('[data-test-subj="lens-legend-location-btn"]').prop('idSelected')
- ).toEqual('xy_location_inside');
+ const { getSelectedLocationOption } = renderLegendLocationSettings({ location: 'inside' });
+ expect(getSelectedLocationOption()).toHaveTextContent('Inside');
});
it('should have called the onLocationChange function on ButtonGroup change', () => {
- const newProps = {
- ...props,
- location: 'inside',
- } as LegendLocationSettingsProps;
- const component = shallow( );
- component
- .find('[data-test-subj="lens-legend-location-btn"]')
- .simulate('change', 'xy_location_outside');
- expect(props.onLocationChange).toHaveBeenCalled();
+ renderLegendLocationSettings({ location: 'inside' });
+ fireEvent.click(screen.getByRole('button', { name: 'Outside' }));
+ expect(defaultProps.onLocationChange).toHaveBeenCalled();
});
it('should default the alignment to top right when no value is given', () => {
- const newProps = {
- ...props,
- location: 'inside',
- } as LegendLocationSettingsProps;
- const component = shallow( );
- expect(
- component.find('[data-test-subj="lens-legend-inside-alignment-btn"]').prop('idSelected')
- ).toEqual('xy_location_alignment_top_right');
+ const { getSelectedAlignmentOption } = renderLegendLocationSettings({ location: 'inside' });
+ expect(getSelectedAlignmentOption()).toHaveTextContent('Top right');
});
it('should have called the onAlignmentChange function on ButtonGroup change', () => {
- const newProps = {
- ...props,
- onAlignmentChange: jest.fn(),
- location: 'inside',
- } as LegendLocationSettingsProps;
- const component = shallow( );
- component
- .find('[data-test-subj="lens-legend-inside-alignment-btn"]')
- .simulate('change', 'xy_location_alignment_top_left');
- expect(newProps.onAlignmentChange).toHaveBeenCalled();
+ renderLegendLocationSettings({ location: 'inside' });
+ fireEvent.click(screen.getByRole('button', { name: 'Top left' }));
+ expect(defaultProps.onAlignmentChange).toHaveBeenCalled();
});
- it('should hide the components when is Disabled is true', () => {
- const newProps = {
- ...props,
- location: 'inside',
- isDisabled: true,
- } as LegendLocationSettingsProps;
- const component = shallow( );
- expect(component.exists('[data-test-subj="lens-legend-location-btn"]')).toEqual(false);
- expect(component.exists('[data-test-subj="lens-legend-inside-alignment-btn"]')).toEqual(false);
+ it('should hide the component if isDisabled prop is true', () => {
+ const { container } = renderLegendLocationSettings({ isDisabled: true });
+ expect(container).toBeEmptyDOMElement();
});
});
diff --git a/x-pack/plugins/lens/public/shared_components/legend/location/legend_location_settings.tsx b/x-pack/plugins/lens/public/shared_components/legend/location/legend_location_settings.tsx
index 5e4ef239b4295..039f778d7439e 100644
--- a/x-pack/plugins/lens/public/shared_components/legend/location/legend_location_settings.tsx
+++ b/x-pack/plugins/lens/public/shared_components/legend/location/legend_location_settings.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiFormRow, EuiButtonGroup } from '@elastic/eui';
+import { EuiFormRow, EuiButtonGroup, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { VerticalAlignment, HorizontalAlignment, Position } from '@elastic/charts';
export interface LegendLocationSettingsProps {
@@ -46,33 +46,33 @@ export interface LegendLocationSettingsProps {
}
const toggleButtonsIcons = [
- {
- id: Position.Top,
- label: i18n.translate('xpack.lens.shared.legendPositionTop', {
- defaultMessage: 'Top',
- }),
- iconType: 'arrowUp',
- },
{
id: Position.Right,
label: i18n.translate('xpack.lens.shared.legendPositionRight', {
defaultMessage: 'Right',
}),
- iconType: 'arrowRight',
- },
- {
- id: Position.Bottom,
- label: i18n.translate('xpack.lens.shared.legendPositionBottom', {
- defaultMessage: 'Bottom',
- }),
- iconType: 'arrowDown',
+ iconType: 'sortRight',
},
{
id: Position.Left,
label: i18n.translate('xpack.lens.shared.legendPositionLeft', {
defaultMessage: 'Left',
}),
- iconType: 'arrowLeft',
+ iconType: 'sortLeft',
+ },
+ {
+ id: Position.Top,
+ label: i18n.translate('xpack.lens.shared.legendPositionTop', {
+ defaultMessage: 'Top',
+ }),
+ iconType: 'sortUp',
+ },
+ {
+ id: Position.Bottom,
+ label: i18n.translate('xpack.lens.shared.legendPositionBottom', {
+ defaultMessage: 'Bottom',
+ }),
+ iconType: 'sortDown',
},
];
@@ -151,80 +151,78 @@ export const LegendLocationSettings: React.FunctionComponent
{location && (
- value === location)!.id}
- onChange={(optionId) => {
- const newLocation = locationOptions.find(({ id }) => id === optionId)!.value;
- onLocationChange(newLocation);
- }}
- />
+
+
+ value === location)!.id}
+ onChange={(optionId) => {
+ const newLocation = locationOptions.find(({ id }) => id === optionId)!.value;
+ onLocationChange(newLocation);
+ }}
+ />
+
+
+ <>
+ {(!location || location === 'outside') && (
+
+ )}
+ {location === 'inside' && (
+ value === alignment)!.id
+ }
+ onChange={(optionId) => {
+ const newAlignment = locationAlignmentButtonsIcons.find(
+ ({ id }) => id === optionId
+ )!.value;
+ onAlignmentChange(newAlignment);
+ }}
+ isIconOnly
+ />
+ )}
+ >
+
+
)}
-
- <>
- {(!location || location === 'outside') && (
-
- )}
- {location === 'inside' && (
- value === alignment)!.id
- }
- onChange={(optionId) => {
- const newAlignment = locationAlignmentButtonsIcons.find(
- ({ id }) => id === optionId
- )!.value;
- onAlignmentChange(newAlignment);
- }}
- isIconOnly
- />
- )}
- >
-
>
);
};
diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx
index 98a0eb609f9a8..f2abfd971463b 100644
--- a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx
+++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx
@@ -7,7 +7,7 @@
import './toolbar_popover.scss';
import React, { useState } from 'react';
-import { EuiFlexItem, EuiPopover, EuiPopoverTitle, IconType } from '@elastic/eui';
+import { EuiFlexItem, EuiPopover, EuiPopoverProps, EuiPopoverTitle, IconType } from '@elastic/eui';
import { ToolbarButton, ToolbarButtonProps } from '@kbn/shared-ux-button-toolbar';
import { EuiIconLegend } from '@kbn/chart-icons';
@@ -19,7 +19,7 @@ const typeToIconMap: { [type: string]: string | IconType } = {
visualOptions: 'brush',
};
-export interface ToolbarPopoverProps {
+export type ToolbarPopoverProps = Partial & {
/**
* Determines popover title
*/
@@ -39,7 +39,7 @@ export interface ToolbarPopoverProps {
buttonDataTestSubj?: string;
panelClassName?: string;
handleClose?: () => void;
-}
+};
export const ToolbarPopover: React.FunctionComponent = ({
children,
@@ -48,10 +48,11 @@ export const ToolbarPopover: React.FunctionComponent = ({
isDisabled = false,
groupPosition,
buttonDataTestSubj,
- panelClassName = 'lnsVisToolbar__popover',
handleClose,
+ panelClassName = 'lnsVisToolbar__popover',
+ ...euiPopoverProps
}) => {
- const [open, setOpen] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
const iconType: string | IconType = typeof type === 'string' ? typeToIconMap[type] : type;
@@ -66,7 +67,7 @@ export const ToolbarPopover: React.FunctionComponent = ({
as={'iconButton'}
iconType={iconType}
onClick={() => {
- setOpen(!open);
+ setIsOpen(!isOpen);
}}
label={title}
aria-label={title}
@@ -75,12 +76,13 @@ export const ToolbarPopover: React.FunctionComponent = ({
data-test-subj={buttonDataTestSubj}
/>
}
- isOpen={open}
+ isOpen={isOpen}
closePopover={() => {
- setOpen(false);
+ setIsOpen(false);
handleClose?.();
}}
anchorPosition="downRight"
+ {...euiPopoverProps}
>
{title}
{children}
diff --git a/x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx b/x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx
index 929478a67beb1..63cf76183778a 100644
--- a/x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx
+++ b/x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx
@@ -78,7 +78,6 @@ export const ValueLabelsSettings: FC = ({
isFullWidth
legend={label}
data-test-subj="lens-value-labels-visibility-btn"
- name="valueLabelsDisplay"
buttonSize="compressed"
options={valueLabelsOptions}
idSelected={isSelected}
diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx
index f431c757c3c73..2ffe06144ff48 100644
--- a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx
@@ -98,7 +98,6 @@ export function TableDimensionEditor(
defaultMessage: 'Text alignment',
})}
data-test-subj="lnsDatatable_alignment_groups"
- name="alignment"
buttonSize="compressed"
options={[
{
@@ -148,7 +147,6 @@ export function TableDimensionEditor(
defaultMessage: 'Color by value',
})}
data-test-subj="lnsDatatable_dynamicColoring_groups"
- name="dynamicColoring"
buttonSize="compressed"
options={[
{
diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/toolbar.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/toolbar.test.tsx
index 8807929360c29..484fff9e6d1bd 100644
--- a/x-pack/plugins/lens/public/visualizations/datatable/components/toolbar.test.tsx
+++ b/x-pack/plugins/lens/public/visualizations/datatable/components/toolbar.test.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { getButtonGroupInputValue } from '@kbn/test-eui-helpers';
+import { getSelectedButtonInGroup } from '@kbn/test-eui-helpers';
import { DataTableToolbar } from './toolbar';
import { DatatableVisualizationState } from '../visualization';
import { FramePublicAPI, VisualizationToolbarProps } from '../../../types';
@@ -72,10 +72,10 @@ describe('datatable toolbar', () => {
return {
...rtlRender,
togglePopover,
- getRowHeightValue: getButtonGroupInputValue(ROW_HEIGHT_SETTINGS_TEST_ID),
+ getRowHeightValue: getSelectedButtonInGroup(ROW_HEIGHT_SETTINGS_TEST_ID),
getRowHeightCustomValue: () => getNumberInput(ROW_HEIGHT_SETTINGS_TEST_ID),
selectRowHeightOption: selectOptionFromButtonGroup(ROW_HEIGHT_SETTINGS_TEST_ID),
- getHeaderHeightValue: getButtonGroupInputValue(HEADER_HEIGHT_SETTINGS_TEST_ID),
+ getHeaderHeightValue: getSelectedButtonInGroup(HEADER_HEIGHT_SETTINGS_TEST_ID),
getHeaderHeightCustomValue: () => getNumberInput(HEADER_HEIGHT_SETTINGS_TEST_ID),
selectHeaderHeightOption: selectOptionFromButtonGroup(HEADER_HEIGHT_SETTINGS_TEST_ID),
getPaginationSwitch,
diff --git a/x-pack/plugins/lens/public/visualizations/legacy_metric/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/legacy_metric/dimension_editor.tsx
index 832118a899ae7..4fc69a60a5372 100644
--- a/x-pack/plugins/lens/public/visualizations/legacy_metric/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/visualizations/legacy_metric/dimension_editor.tsx
@@ -71,7 +71,6 @@ export function MetricDimensionEditor(
defaultMessage: 'Color by value',
})}
data-test-subj="lnsLegacyMetric_dynamicColoring_groups"
- name="dynamicColoring"
buttonSize="compressed"
options={[
{
diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx
index f9b9fd9e1b363..43179e9cba51d 100644
--- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx
@@ -580,7 +580,6 @@ export function DimensionEditorAdditionalSection({
defaultMessage: 'Bar orientation',
})}
data-test-subj="lnsMetric_progress_direction_buttons"
- name="alignment"
options={[
{
id: `${idPrefix}vertical`,
diff --git a/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx b/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx
index fddf0280b9539..48a9030097940 100644
--- a/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx
+++ b/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx
@@ -228,7 +228,6 @@ export function PieToolbar(props: VisualizationToolbarProps
value === orientation)!.id}
diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx
index e2ecac16daee1..2ee2fa6ac2f80 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx
@@ -271,7 +271,6 @@ export function DataDimensionEditor(
defaultMessage: 'Axis side',
})}
data-test-subj="lnsXY_axisSide_groups"
- name="axisSide"
buttonSize="compressed"
options={[
{
diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx
index 411d3049699ad..83c28544913b4 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx
+++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx
@@ -172,7 +172,6 @@ export const FillSetting = ({
defaultMessage: 'Fill',
})}
data-test-subj="lnsXY_fill"
- name="fill"
buttonSize="compressed"
options={getFillPositionOptions({ isHorizontal, axisMode: currentConfig?.axisMode })}
idSelected={`${idPrefix}${currentConfig?.fill || 'none'}`}
diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx
index e2b149dfc0eb6..e15434fc5c094 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx
+++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx
@@ -119,7 +119,6 @@ export function MarkerDecorationPosition({
defaultMessage: 'Decoration position',
})}
data-test-subj="lnsXY_markerPosition"
- name="markerPosition"
isDisabled={!hasIcon(currentConfig?.icon) && !currentConfig?.textVisibility}
buttonSize="compressed"
options={getIconPositionOptions({
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 359026860d16f..09e5421c56b7e 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -23820,13 +23820,11 @@
"xpack.lens.shared.chartValueLabelVisibilityTooltip": "Si l'espace est insuffisant, les étiquettes de valeurs pourront être masquées",
"xpack.lens.shared.curveLabel": "Options visuelles",
"xpack.lens.shared.legendAlignmentLabel": "Alignement",
- "xpack.lens.shared.legendInsideAlignmentLabel": "Alignement",
"xpack.lens.shared.legendInsideColumnsLabel": "Nombre de colonnes",
"xpack.lens.shared.legendInsideLocationAlignmentLabel": "Alignement",
"xpack.lens.shared.legendLabel": "Légende",
"xpack.lens.shared.legendLocationBottomLeft": "En bas à gauche",
"xpack.lens.shared.legendLocationBottomRight": "En bas à droite",
- "xpack.lens.shared.legendLocationLabel": "Emplacement",
"xpack.lens.shared.legendLocationTopLeft": "En haut à gauche",
"xpack.lens.shared.legendLocationTopRight": "En haut à droite",
"xpack.lens.shared.legendPositionBottom": "Bas",
@@ -23840,14 +23838,12 @@
"xpack.lens.shared.legendSizeSetting.legendSizeOptions.medium": "Moyenne",
"xpack.lens.shared.legendSizeSetting.legendSizeOptions.small": "Petite",
"xpack.lens.shared.legendVisibilityLabel": "Affichage",
- "xpack.lens.shared.maxLinesLabel": "Nombre maximal de lignes",
"xpack.lens.shared.metric.appearanceLabel": "Apparence",
"xpack.lens.shared.nestedLegendLabel": "Imbriqué",
"xpack.lens.shared.overwriteAxisTitle": "Écraser le titre de l'axe",
"xpack.lens.shared.tickLabels": "Étiquettes de graduation",
"xpack.lens.shared.ticksPositionOptions": "Coches sur les bandes",
"xpack.lens.shared.ticksPositionOptionsTooltip": "Place les coches sur chaque bordure de bande au lieu de les répartir de manière homogène.",
- "xpack.lens.shared.truncateLegend": "Tronquer le texte",
"xpack.lens.shared.valueInLegendLabel": "Afficher la valeur",
"xpack.lens.shared.valueLabelsVisibility.auto": "Masquer",
"xpack.lens.shared.valueLabelsVisibility.inside": "Afficher",
@@ -45117,4 +45113,4 @@
"xpack.serverlessObservability.nav.projectSettings": "Paramètres de projet",
"xpack.serverlessObservability.nav.synthetics": "Synthetics"
}
-}
+}
\ No newline at end of file
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 2c3c3d2cf827b..e53d1a2d2b10c 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -23795,13 +23795,11 @@
"xpack.lens.shared.chartValueLabelVisibilityTooltip": "十分なスペースがない場合、値ラベルが非表示になることがあります。",
"xpack.lens.shared.curveLabel": "視覚オプション",
"xpack.lens.shared.legendAlignmentLabel": "アラインメント",
- "xpack.lens.shared.legendInsideAlignmentLabel": "アラインメント",
"xpack.lens.shared.legendInsideColumnsLabel": "列の数",
"xpack.lens.shared.legendInsideLocationAlignmentLabel": "アラインメント",
"xpack.lens.shared.legendLabel": "凡例",
"xpack.lens.shared.legendLocationBottomLeft": "左下",
"xpack.lens.shared.legendLocationBottomRight": "右下",
- "xpack.lens.shared.legendLocationLabel": "場所",
"xpack.lens.shared.legendLocationTopLeft": "左上",
"xpack.lens.shared.legendLocationTopRight": "右上",
"xpack.lens.shared.legendPositionBottom": "一番下",
@@ -23815,14 +23813,12 @@
"xpack.lens.shared.legendSizeSetting.legendSizeOptions.medium": "中",
"xpack.lens.shared.legendSizeSetting.legendSizeOptions.small": "小",
"xpack.lens.shared.legendVisibilityLabel": "表示",
- "xpack.lens.shared.maxLinesLabel": "最大行",
"xpack.lens.shared.metric.appearanceLabel": "見た目",
"xpack.lens.shared.nestedLegendLabel": "ネスト済み",
"xpack.lens.shared.overwriteAxisTitle": "軸タイトルを上書き",
"xpack.lens.shared.tickLabels": "目盛ラベル",
"xpack.lens.shared.ticksPositionOptions": "帯の目盛",
"xpack.lens.shared.ticksPositionOptionsTooltip": "目盛を均等に分布するのではなく、各帯の境界に目盛を表示します",
- "xpack.lens.shared.truncateLegend": "テキストを切り捨て",
"xpack.lens.shared.valueInLegendLabel": "値を表示",
"xpack.lens.shared.valueLabelsVisibility.auto": "非表示",
"xpack.lens.shared.valueLabelsVisibility.inside": "表示",
@@ -45087,4 +45083,4 @@
"xpack.serverlessObservability.nav.projectSettings": "プロジェクト設定",
"xpack.serverlessObservability.nav.synthetics": "Synthetics"
}
-}
+}
\ No newline at end of file
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 448da6450b4d6..323dac81ec7c6 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -23828,13 +23828,11 @@
"xpack.lens.shared.chartValueLabelVisibilityTooltip": "如果没有足够的空间,可能会隐藏值标签",
"xpack.lens.shared.curveLabel": "视觉选项",
"xpack.lens.shared.legendAlignmentLabel": "对齐方式",
- "xpack.lens.shared.legendInsideAlignmentLabel": "对齐方式",
"xpack.lens.shared.legendInsideColumnsLabel": "列数目",
"xpack.lens.shared.legendInsideLocationAlignmentLabel": "对齐方式",
"xpack.lens.shared.legendLabel": "图例",
"xpack.lens.shared.legendLocationBottomLeft": "左下方",
"xpack.lens.shared.legendLocationBottomRight": "右下方",
- "xpack.lens.shared.legendLocationLabel": "位置",
"xpack.lens.shared.legendLocationTopLeft": "左上方",
"xpack.lens.shared.legendLocationTopRight": "右上方",
"xpack.lens.shared.legendPositionBottom": "底部",
@@ -23848,14 +23846,12 @@
"xpack.lens.shared.legendSizeSetting.legendSizeOptions.medium": "中",
"xpack.lens.shared.legendSizeSetting.legendSizeOptions.small": "小",
"xpack.lens.shared.legendVisibilityLabel": "显示",
- "xpack.lens.shared.maxLinesLabel": "最大行数",
"xpack.lens.shared.metric.appearanceLabel": "外观",
"xpack.lens.shared.nestedLegendLabel": "嵌套",
"xpack.lens.shared.overwriteAxisTitle": "覆盖轴标题",
"xpack.lens.shared.tickLabels": "刻度标签",
"xpack.lens.shared.ticksPositionOptions": "带上的刻度",
"xpack.lens.shared.ticksPositionOptionsTooltip": "将刻度放在每个带边框上,而不是平均分布",
- "xpack.lens.shared.truncateLegend": "截断文本",
"xpack.lens.shared.valueInLegendLabel": "显示值",
"xpack.lens.shared.valueLabelsVisibility.auto": "隐藏",
"xpack.lens.shared.valueLabelsVisibility.inside": "显示",
@@ -45135,4 +45131,4 @@
"xpack.serverlessObservability.nav.projectSettings": "项目设置",
"xpack.serverlessObservability.nav.synthetics": "Synthetics"
}
-}
+}
\ No newline at end of file
From 2b344d27979009300a0fdc9e8aa18aff28a9a66a Mon Sep 17 00:00:00 2001
From: Nick Partridge
Date: Wed, 10 Apr 2024 10:28:03 -0700
Subject: [PATCH 26/55] [FTR] Update `getAttribute` method return (#179715)
---
.../services/test_subjects.ts | 14 ++++++++++----
.../web_element_wrapper/web_element_wrapper.ts | 11 ++++++-----
test/examples/state_sync/todo_app.ts | 4 ++--
.../apps/dashboard/group3/dashboard_state.ts | 4 ++--
test/functional/apps/dashboard/group5/share.ts | 3 +--
.../common/control_group_apply_button.ts | 2 +-
.../controls/common/control_group_settings.ts | 18 ++++++++++--------
.../discover/group1/_discover_histogram.ts | 9 ++++-----
.../discover/group2/_data_grid_field_tokens.ts | 4 +++-
.../apps/kibana_overview/_analytics.ts | 2 +-
.../apps/kibana_overview/_solutions.ts | 2 +-
test/functional/page_objects/console_page.ts | 12 ++++++------
test/functional/page_objects/dashboard_page.ts | 6 +++++-
.../page_objects/dashboard_page_controls.ts | 11 +++++++----
test/functional/page_objects/discover_page.ts | 2 +-
test/functional/page_objects/home_page.ts | 6 +++---
.../management/saved_objects_page.ts | 2 +-
test/functional/page_objects/settings_page.ts | 4 ++--
test/functional/page_objects/share_page.ts | 2 +-
test/functional/page_objects/tag_cloud_page.ts | 2 +-
.../page_objects/unified_field_list.ts | 10 ++++++----
.../page_objects/visual_builder_page.ts | 10 +++++-----
.../page_objects/visualize_chart_page.ts | 2 +-
test/functional/services/combo_box.ts | 2 +-
.../dashboard/panel_drilldown_actions.ts | 2 +-
test/functional/services/filter_bar.ts | 4 ++--
test/functional/services/global_nav.ts | 6 ++----
test/functional/services/query_bar.ts | 2 +-
test/functional/services/selectable.ts | 2 +-
.../services/visualizations/elastic_chart.ts | 6 ++----
.../services/visualizations/pie_chart.ts | 2 +-
.../add_cis_integration_form_page.ts | 2 +-
.../page_objects/findings_page.ts | 4 +++-
x-pack/test/custom_branding/tests/settings.ts | 4 ++--
.../search_examples/partial_results_example.ts | 2 +-
.../drilldowns/explore_data_chart_action.ts | 2 +-
.../test/functional/apps/discover/reporting.ts | 2 +-
.../test/functional/page_objects/gis_page.ts | 6 +++---
.../test/functional/page_objects/graph_page.ts | 14 +++++++-------
.../test/functional/page_objects/lens_page.ts | 11 +++++++----
.../functional/page_objects/reporting_page.ts | 8 ++++++++
.../search_sessions_management_page.ts | 2 +-
.../page_objects/tag_management_page.ts | 7 ++++---
.../functional/page_objects/watcher_page.ts | 2 +-
.../functional/services/ml/anomalies_table.ts | 2 +-
.../services/ml/common_table_service.ts | 6 ++++--
.../test/functional/services/ml/common_ui.ts | 2 +-
.../services/ml/job_wizard_advanced.ts | 6 +++---
.../services/ml/stack_management_jobs.ts | 2 +-
.../functional/services/uptime/settings.ts | 6 +++---
.../page_objects/rule_details.ts | 2 +-
.../page_objects/triggers_actions_ui_page.ts | 2 +-
.../apps/observability/pages/rules_page.ts | 3 ++-
.../tests/apps/discover/async_search.ts | 2 +-
.../page_objects/svl_rule_details_ui_page.ts | 2 +-
.../svl_triggers_actions_ui_page.ts | 2 +-
.../discover/group1/_discover_histogram.ts | 9 ++++-----
.../common/discover/x_pack/reporting.ts | 2 +-
.../search_examples/partial_results_example.ts | 2 +-
59 files changed, 157 insertions(+), 127 deletions(-)
diff --git a/packages/kbn-ftr-common-functional-ui-services/services/test_subjects.ts b/packages/kbn-ftr-common-functional-ui-services/services/test_subjects.ts
index 730b7a692aabe..39cd21f284132 100644
--- a/packages/kbn-ftr-common-functional-ui-services/services/test_subjects.ts
+++ b/packages/kbn-ftr-common-functional-ui-services/services/test_subjects.ts
@@ -21,6 +21,10 @@ interface SetValueOptions {
typeCharByChar?: boolean;
}
+export function nonNullable(v: T): v is NonNullable {
+ return v != null;
+}
+
export class TestSubjects extends FtrService {
public readonly log = this.ctx.getService('log');
public readonly retry = this.ctx.getService('retry');
@@ -226,9 +230,11 @@ export class TestSubjects extends FtrService {
public async getAttributeAll(selector: string, attribute: string): Promise {
this.log.debug(`TestSubjects.getAttributeAll(${selector}, ${attribute})`);
- return await this._mapAll(selector, async (element: WebElementWrapper) => {
- return await element.getAttribute(attribute);
- });
+ return (
+ await this._mapAll(selector, async (element: WebElementWrapper) => {
+ return await element.getAttribute(attribute);
+ })
+ ).filter(nonNullable);
}
public async getAttribute(
@@ -240,7 +246,7 @@ export class TestSubjects extends FtrService {
findTimeout?: number;
tryTimeout?: number;
}
- ): Promise {
+ ): Promise {
const findTimeout =
(typeof options === 'number' ? options : options?.findTimeout) ??
this.config.get('timeouts.find');
diff --git a/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts b/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts
index 568d8dc5cd879..e7083d7f17587 100644
--- a/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts
+++ b/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts
@@ -231,7 +231,7 @@ export class WebElementWrapper {
* @return {Promise}
*/
public async elementHasClass(className: string): Promise {
- const classes: string = await this._webElement.getAttribute('class');
+ const classes = (await this._webElement.getAttribute('class')) ?? '';
return classes.includes(className);
}
@@ -262,7 +262,7 @@ export class WebElementWrapper {
*/
async clearValueWithKeyboard(options: TypeOptions = { charByChar: false }) {
const value = await this.getAttribute('value');
- if (!value.length) {
+ if (!value?.length) {
return;
}
@@ -344,10 +344,11 @@ export class WebElementWrapper {
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#getAttribute
*
* @param {string} name
+ * @return {Promise}
*/
- public async getAttribute(name: string) {
- return await this.retryCall(async function getAttribute(wrapper) {
- return await wrapper._webElement.getAttribute(name);
+ public async getAttribute(name: string): Promise {
+ return await this.retryCall(async function getAttribute(wrapper): Promise {
+ return await wrapper._webElement.getAttribute(name); // this returns null if not found
});
}
diff --git a/test/examples/state_sync/todo_app.ts b/test/examples/state_sync/todo_app.ts
index 68ed23c5a3eda..f4a851deba69d 100644
--- a/test/examples/state_sync/todo_app.ts
+++ b/test/examples/state_sync/todo_app.ts
@@ -32,7 +32,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
it('links are rendered correctly and state is preserved in links', async () => {
const getHrefByLinkTestSubj = async (linkTestSubj: string) =>
- (await testSubjects.find(linkTestSubj)).getAttribute('href');
+ (await (await testSubjects.find(linkTestSubj)).getAttribute('href')) ?? '';
await expectPathname(await getHrefByLinkTestSubj('filterLinkCompleted'), '/completed');
await expectPathname(
@@ -115,7 +115,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
it('Links are rendered correctly and state is preserved in links', async () => {
const getHrefByLinkTestSubj = async (linkTestSubj: string) =>
- (await testSubjects.find(linkTestSubj)).getAttribute('href');
+ (await (await testSubjects.find(linkTestSubj)).getAttribute('href')) ?? '';
await expectHashPathname(await getHrefByLinkTestSubj('filterLinkCompleted'), '/completed');
await expectHashPathname(
await getHrefByLinkTestSubj('filterLinkNotCompleted'),
diff --git a/test/functional/apps/dashboard/group3/dashboard_state.ts b/test/functional/apps/dashboard/group3/dashboard_state.ts
index 650d2e6b79269..e8cdc3b3aa2cb 100644
--- a/test/functional/apps/dashboard/group3/dashboard_state.ts
+++ b/test/functional/apps/dashboard/group3/dashboard_state.ts
@@ -295,7 +295,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('when removing a panel', async function () {
await PageObjects.dashboard.waitForRenderComplete();
- const currentUrl = await getUrlFromShare();
+ const currentUrl = (await getUrlFromShare()) ?? '';
const newUrl = updateAppStateQueryParam(
currentUrl,
(appState: Partial) => {
@@ -319,7 +319,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await queryBar.clearQuery();
await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME);
await enableNewChartLibraryDebug();
- originalPieSliceStyle = await pieChart.getPieSliceStyle(`80,000`);
+ originalPieSliceStyle = (await pieChart.getPieSliceStyle(`80,000`)) ?? '';
});
it('updates a pie slice color on a hard refresh', async function () {
diff --git a/test/functional/apps/dashboard/group5/share.ts b/test/functional/apps/dashboard/group5/share.ts
index 45bb5cd80c508..298a6749e4896 100644
--- a/test/functional/apps/dashboard/group5/share.ts
+++ b/test/functional/apps/dashboard/group5/share.ts
@@ -52,8 +52,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
if (mode === 'savedObject') {
await PageObjects.share.exportAsSavedObject();
}
- const sharedUrl = await PageObjects.share.getSharedUrl();
- return sharedUrl;
+ return PageObjects.share.getSharedUrl();
};
describe('share dashboard', () => {
diff --git a/test/functional/apps/dashboard_elements/controls/common/control_group_apply_button.ts b/test/functional/apps/dashboard_elements/controls/common/control_group_apply_button.ts
index 7702202309b70..40e75ac70ff09 100644
--- a/test/functional/apps/dashboard_elements/controls/common/control_group_apply_button.ts
+++ b/test/functional/apps/dashboard_elements/controls/common/control_group_apply_button.ts
@@ -189,7 +189,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
describe('time slider selections', () => {
- let valueBefore: string;
+ let valueBefore: string | null;
before(async () => {
valueBefore = await dashboardControls.getTimeSliceFromTimeSlider();
diff --git a/test/functional/apps/dashboard_elements/controls/common/control_group_settings.ts b/test/functional/apps/dashboard_elements/controls/common/control_group_settings.ts
index f4359f9887c59..80a8bc8148977 100644
--- a/test/functional/apps/dashboard_elements/controls/common/control_group_settings.ts
+++ b/test/functional/apps/dashboard_elements/controls/common/control_group_settings.ts
@@ -92,14 +92,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const getRange = async () => {
await dashboardControls.rangeSliderWaitForLoading(rangeSliderId);
- const lower = await dashboardControls.rangeSliderGetLowerBoundAttribute(
- rangeSliderId,
- 'placeholder'
- );
- const upper = await dashboardControls.rangeSliderGetUpperBoundAttribute(
- rangeSliderId,
- 'placeholder'
- );
+ const lower =
+ (await dashboardControls.rangeSliderGetLowerBoundAttribute(
+ rangeSliderId,
+ 'placeholder'
+ )) ?? '0';
+ const upper =
+ (await dashboardControls.rangeSliderGetUpperBoundAttribute(
+ rangeSliderId,
+ 'placeholder'
+ )) ?? '0';
return parseInt(upper, 10) - parseInt(lower, 10);
};
diff --git a/test/functional/apps/discover/group1/_discover_histogram.ts b/test/functional/apps/discover/group1/_discover_histogram.ts
index ad5563e78f918..e695a7d97ebaa 100644
--- a/test/functional/apps/discover/group1/_discover_histogram.ts
+++ b/test/functional/apps/discover/group1/_discover_histogram.ts
@@ -304,10 +304,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.saveSearch(savedSearch);
await PageObjects.discover.chooseBreakdownField('extension.keyword');
await PageObjects.discover.setChartInterval('Second');
- let requestData = await testSubjects.getAttribute(
- 'unifiedHistogramChart',
- 'data-request-data'
- );
+ let requestData =
+ (await testSubjects.getAttribute('unifiedHistogramChart', 'data-request-data')) ?? '';
expect(JSON.parse(requestData)).to.eql({
dataViewId: 'long-window-logstash-*',
timeField: '@timestamp',
@@ -318,7 +316,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.revertUnsavedChanges();
await PageObjects.discover.waitUntilSearchingHasFinished();
- requestData = await testSubjects.getAttribute('unifiedHistogramChart', 'data-request-data');
+ requestData =
+ (await testSubjects.getAttribute('unifiedHistogramChart', 'data-request-data')) ?? '';
expect(JSON.parse(requestData)).to.eql({
dataViewId: 'long-window-logstash-*',
timeField: '@timestamp',
diff --git a/test/functional/apps/discover/group2/_data_grid_field_tokens.ts b/test/functional/apps/discover/group2/_data_grid_field_tokens.ts
index 1d60fe3cd8c79..895fbee621cb1 100644
--- a/test/functional/apps/discover/group2/_data_grid_field_tokens.ts
+++ b/test/functional/apps/discover/group2/_data_grid_field_tokens.ts
@@ -48,7 +48,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const fieldIcons = await element.findAllByCssSelector('.kbnFieldIcon svg');
firstFieldIcons = await Promise.all(
- fieldIcons.slice(0, 10).map((fieldIcon) => fieldIcon.getAttribute('aria-label'))
+ fieldIcons.slice(0, 10).map(async (fieldIcon) => {
+ return (await fieldIcon.getAttribute('aria-label')) ?? '';
+ })
).catch((error) => {
log.debug(`error in findFirstFieldIcons: ${error.message}`);
return undefined;
diff --git a/test/functional/apps/kibana_overview/_analytics.ts b/test/functional/apps/kibana_overview/_analytics.ts
index 8fd51106cd239..516f9d9e4850b 100644
--- a/test/functional/apps/kibana_overview/_analytics.ts
+++ b/test/functional/apps/kibana_overview/_analytics.ts
@@ -41,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const verifyImageUrl = async (el: WebElementWrapper, imgName: string) => {
const image = await el.findByCssSelector('img');
- const imageUrl = await image.getAttribute('src');
+ const imageUrl = (await image.getAttribute('src')) ?? '';
expect(imageUrl.includes(imgName)).to.be(true);
};
diff --git a/test/functional/apps/kibana_overview/_solutions.ts b/test/functional/apps/kibana_overview/_solutions.ts
index 9869a295b6deb..75c80c0916d23 100644
--- a/test/functional/apps/kibana_overview/_solutions.ts
+++ b/test/functional/apps/kibana_overview/_solutions.ts
@@ -43,7 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
for (let i = 0; i < solutionCards.length; i++) {
const solutionCard = solutionCards[i];
const image = await solutionCard.findByCssSelector('img');
- const imageSrc = await image.getAttribute('src');
+ const imageSrc = (await image.getAttribute('src')) ?? '';
const match = myRegexp.exec(imageSrc);
myRegexp.lastIndex = 0;
if (match && match.length > 1) {
diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts
index 51e671d26301b..3fdc1e8ab0f74 100644
--- a/test/functional/page_objects/console_page.ts
+++ b/test/functional/page_objects/console_page.ts
@@ -147,7 +147,7 @@ export class ConsolePageObject extends FtrService {
if (!element) return false;
const attribute = await element.getAttribute('style');
- return !attribute.includes('display: none;');
+ return !attribute?.includes('display: none;');
}
public async getAutocompleteSuggestion(index: number = 0) {
@@ -321,7 +321,7 @@ export class ConsolePageObject extends FtrService {
await blocks[blockNumber].click();
await this.retry.waitFor('json block to be collapsed', async () => {
return blocks[blockNumber].getAttribute('class').then((classes) => {
- return classes.includes('ace_closed');
+ return classes?.includes('ace_closed') ?? false;
});
});
}
@@ -336,7 +336,7 @@ export class ConsolePageObject extends FtrService {
await blocks[blockNumber].click();
await this.retry.waitFor('json block to be expanded', async () => {
return blocks[blockNumber].getAttribute('class').then((classes) => {
- return classes.includes('ace_open');
+ return classes?.includes('ace_open') ?? false;
});
});
}
@@ -349,7 +349,7 @@ export class ConsolePageObject extends FtrService {
}
const classes = await blocks[blockNumber].getAttribute('class');
- return classes.includes('ace_open');
+ return classes?.includes('ace_open') ?? false;
}
public async selectCurrentRequest() {
@@ -385,7 +385,7 @@ export class ConsolePageObject extends FtrService {
});
// style attribute looks like this: "top: 0px; height: 18.5px;" height is the line height
- const styleAttribute = await line.getAttribute('style');
+ const styleAttribute = (await line.getAttribute('style')) ?? '';
const height = parseFloat(styleAttribute.replace(/.*height: ([+-]?\d+(\.\d+)?).*/, '$1'));
const top = parseFloat(styleAttribute.replace(/.*top: ([+-]?\d+(\.\d+)?).*/, '$1'));
// calculate the line number by dividing the top position by the line height
@@ -490,7 +490,7 @@ export class ConsolePageObject extends FtrService {
const path = [];
for (const pathPart of requestPath) {
const className = await pathPart.getAttribute('class');
- if (className.includes('ace_param')) {
+ if (className?.includes('ace_param') ?? false) {
// This is a parameter, we don't want to include it in the path
break;
}
diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts
index 13a3a860fe7d9..2b98eedbb62f1 100644
--- a/test/functional/page_objects/dashboard_page.ts
+++ b/test/functional/page_objects/dashboard_page.ts
@@ -696,7 +696,11 @@ export class DashboardPageObject extends FtrService {
const attributeName = 'data-shared-items-count';
const element = await this.find.byCssSelector(`[${attributeName}]`);
if (element) {
- return await element.getAttribute(attributeName);
+ const attribute = await element.getAttribute(attributeName);
+
+ if (!attribute) throw new Error(`no attribute found for [${attributeName}]`);
+
+ return attribute;
}
throw new Error('no element');
diff --git a/test/functional/page_objects/dashboard_page_controls.ts b/test/functional/page_objects/dashboard_page_controls.ts
index 119b347c2ca69..c57539ba2079b 100644
--- a/test/functional/page_objects/dashboard_page_controls.ts
+++ b/test/functional/page_objects/dashboard_page_controls.ts
@@ -65,7 +65,9 @@ export class DashboardPageControls extends FtrService {
public async getAllControlIds() {
const controlFrames = await this.testSubjects.findAll('control-frame');
const ids = await Promise.all(
- controlFrames.map(async (controlFrame) => await controlFrame.getAttribute('data-control-id'))
+ controlFrames.map(
+ async (controlFrame) => (await controlFrame.getAttribute('data-control-id')) ?? ''
+ )
);
this.log.debug('Got all control ids:', ids);
return ids;
@@ -93,7 +95,7 @@ export class DashboardPageControls extends FtrService {
public async clearAllControls() {
const controlIds = await this.getAllControlIds();
for (const controlId of controlIds) {
- await this.removeExistingControl(controlId);
+ if (controlId) await this.removeExistingControl(controlId);
}
}
@@ -162,7 +164,8 @@ export class DashboardPageControls extends FtrService {
false: 'NONE',
};
- const switchState = await this.testSubjects.getAttribute('control-group-chaining', 'checked');
+ const switchState =
+ (await this.testSubjects.getAttribute('control-group-chaining', 'checked')) ?? '';
if (chainingSystem !== switchStateToChainingSystem[switchState]) {
await this.testSubjects.click('control-group-chaining');
}
@@ -432,7 +435,7 @@ export class DashboardPageControls extends FtrService {
this.log.debug(`getting available options count from options list`);
await this.optionsListPopoverWaitForLoading();
const availableOptions = await this.testSubjects.find(`optionsList-control-available-options`);
- return +(await availableOptions.getAttribute('data-option-count'));
+ return +((await availableOptions.getAttribute('data-option-count')) ?? '0');
}
public async optionsListPopoverGetAvailableOptions() {
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index b7de38b825346..6704f56e5e0e1 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -356,7 +356,7 @@ export class DiscoverPageObject extends FtrService {
return await cell.getVisibleText();
} else {
const textContent = await cell.getAttribute('textContent');
- return textContent.trim();
+ return textContent?.trim();
}
})
);
diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts
index 4225f52b16994..0c85c51381b94 100644
--- a/test/functional/page_objects/home_page.ts
+++ b/test/functional/page_objects/home_page.ts
@@ -30,7 +30,7 @@ export class HomePageObject extends FtrService {
async openSampleDataAccordion() {
const accordionButton = await this.testSubjects.find('showSampleDataButton');
- let expandedAttribute = await accordionButton.getAttribute('aria-expanded');
+ let expandedAttribute = (await accordionButton.getAttribute('aria-expanded')) ?? '';
let expanded = expandedAttribute.toLocaleLowerCase().includes('true');
this.log.debug(`Sample data accordion expanded: ${expanded}`);
@@ -38,7 +38,7 @@ export class HomePageObject extends FtrService {
await this.retry.waitFor('sample data according to be expanded', async () => {
this.log.debug(`Opening sample data accordion`);
await accordionButton.click();
- expandedAttribute = await accordionButton.getAttribute('aria-expanded');
+ expandedAttribute = (await accordionButton.getAttribute('aria-expanded')) ?? '';
expanded = expandedAttribute.toLocaleLowerCase().includes('true');
return expanded;
});
@@ -75,7 +75,7 @@ export class HomePageObject extends FtrService {
const panelAttributes = await Promise.all(
solutionPanels.map((panel) => panel.getAttribute('data-test-subj'))
);
- return panelAttributes.map((attributeValue) => attributeValue.split('homSolutionPanel_')[1]);
+ return panelAttributes.map((attributeValue) => attributeValue?.split('homSolutionPanel_')[1]);
}
async goToSampleDataPage() {
diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts
index b58907d13eabc..cbbb70587ea0e 100644
--- a/test/functional/page_objects/management/saved_objects_page.ts
+++ b/test/functional/page_objects/management/saved_objects_page.ts
@@ -283,7 +283,7 @@ export class SavedObjectsPageObject extends FtrService {
let copySaveObjectsElement = null;
const actions = await row.findByClassName('euiTableRowCell--hasActions');
// getting the innerHTML and checking if it 'includes' a string is faster than a timeout looking for each element
- const actionsHTML = await actions.getAttribute('innerHTML');
+ const actionsHTML = (await actions.getAttribute('innerHTML')) ?? '';
if (actionsHTML.includes('euiCollapsedItemActionsButton')) {
menuElement = await row.findByTestSubject('euiCollapsedItemActionsButton');
}
diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts
index fc1064f1b0464..546e08fe31115 100644
--- a/test/functional/page_objects/settings_page.ts
+++ b/test/functional/page_objects/settings_page.ts
@@ -131,7 +131,7 @@ export class SettingsPageObject extends FtrService {
}
async toggleAdvancedSettingCheckbox(propertyName: string, value?: boolean) {
- let curValue: string | undefined;
+ let curValue: string | null;
if (value !== undefined) {
curValue = await this.getAdvancedSettingAriaCheckbox(propertyName);
@@ -669,7 +669,7 @@ export class SettingsPageObject extends FtrService {
// case where we don't want the * appended so we'll remove it if it was added
await field.type(indexPatternName, { charByChar: true });
const tempName = await field.getAttribute('value');
- if (tempName.length > indexPatternName.length) {
+ if (tempName?.length ?? 0 > indexPatternName.length) {
await field.type(this.browser.keys.DELETE, { charByChar: true });
}
}
diff --git a/test/functional/page_objects/share_page.ts b/test/functional/page_objects/share_page.ts
index ce1dc4c45e21f..f0f5fa0180f21 100644
--- a/test/functional/page_objects/share_page.ts
+++ b/test/functional/page_objects/share_page.ts
@@ -52,7 +52,7 @@ export class SharePageObject extends FtrService {
async getSharedUrl() {
await this.openPermaLinks();
- return await this.testSubjects.getAttribute('copyShareUrlButton', 'data-share-url');
+ return (await this.testSubjects.getAttribute('copyShareUrlButton', 'data-share-url')) ?? '';
}
async createShortUrlExistOrFail() {
diff --git a/test/functional/page_objects/tag_cloud_page.ts b/test/functional/page_objects/tag_cloud_page.ts
index ba7648b323ca9..56063712c6fa8 100644
--- a/test/functional/page_objects/tag_cloud_page.ts
+++ b/test/functional/page_objects/tag_cloud_page.ts
@@ -36,7 +36,7 @@ export class TagCloudPageObject extends FtrService {
const tags = await this.find.allByCssSelector('text');
async function returnTagSize(tag: WebElementWrapper) {
const style = await tag.getAttribute('style');
- const fontSize = style.match(/font-size: ([^;]*);/);
+ const fontSize = style?.match(/font-size: ([^;]*);/);
return fontSize ? fontSize[1] : '';
}
return await Promise.all(tags.map(returnTagSize));
diff --git a/test/functional/page_objects/unified_field_list.ts b/test/functional/page_objects/unified_field_list.ts
index 152a1b4c1c660..d09ac99e30790 100644
--- a/test/functional/page_objects/unified_field_list.ts
+++ b/test/functional/page_objects/unified_field_list.ts
@@ -36,9 +36,11 @@ export class UnifiedFieldListPageObject extends FtrService {
}
public async getSidebarAriaDescription(): Promise {
- return await (
- await this.testSubjects.find('fieldListGrouped__ariaDescription')
- ).getAttribute('innerText');
+ return (
+ (await (
+ await this.testSubjects.find('fieldListGrouped__ariaDescription')
+ ).getAttribute('innerText')) ?? ''
+ );
}
public async cleanSidebarLocalStorage(): Promise {
@@ -78,7 +80,7 @@ export class UnifiedFieldListPageObject extends FtrService {
}
return Promise.all(
- elements.map(async (element) => await element.getAttribute('data-attr-field'))
+ elements.map(async (element) => (await element.getAttribute('data-attr-field')) ?? '')
);
}
diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts
index 89f1a2e9389c2..d7bb97c5c8809 100644
--- a/test/functional/page_objects/visual_builder_page.ts
+++ b/test/functional/page_objects/visual_builder_page.ts
@@ -373,7 +373,7 @@ export class VisualBuilderPageObject extends FtrService {
return await gaugeCount.getVisibleText();
}
- public async getGaugeColor(isInner = false): Promise {
+ public async getGaugeColor(isInner = false): Promise {
await this.visChart.waitForVisualizationRenderingStabilized();
const gaugeColoredCircle = await this.testSubjects.find(`gaugeCircle${isInner ? 'Inner' : ''}`);
return await gaugeColoredCircle.getAttribute('stroke');
@@ -395,7 +395,7 @@ export class VisualBuilderPageObject extends FtrService {
return await gaugeCount.getVisibleText();
}
- public async getTopNBarStyle(nth: number = 0): Promise {
+ public async getTopNBarStyle(nth: number = 0): Promise {
await this.visChart.waitForVisualizationRenderingStabilized();
const topNBars = await this.testSubjects.findAll('topNInnerBar');
return await topNBars[nth].getAttribute('style');
@@ -756,19 +756,19 @@ export class VisualBuilderPageObject extends FtrService {
});
}
- public async getBackgroundStyle(): Promise {
+ public async getBackgroundStyle(): Promise {
await this.visChart.waitForVisualizationRenderingStabilized();
const visualization = await this.find.byClassName('tvbVis');
return await visualization.getAttribute('style');
}
- public async getMetricValueStyle(): Promise {
+ public async getMetricValueStyle(): Promise {
await this.visChart.waitForVisualizationRenderingStabilized();
const metricValue = await this.testSubjects.find('tsvbMetricValue');
return await metricValue.getAttribute('style');
}
- public async getGaugeValueStyle(): Promise {
+ public async getGaugeValueStyle(): Promise {
await this.visChart.waitForVisualizationRenderingStabilized();
const metricValue = await this.testSubjects.find('gaugeValue');
return await metricValue.getAttribute('style');
diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts
index 093fcdb9edbf9..19991de54da03 100644
--- a/test/functional/page_objects/visualize_chart_page.ts
+++ b/test/functional/page_objects/visualize_chart_page.ts
@@ -63,7 +63,7 @@ export class VisualizeChartPageObject extends FtrService {
// check if enabled but not a line, area, histogram or pie chart
if (await this.find.existsByCssSelector('.visLib__chart', 1)) {
const chart = await this.find.byCssSelector('.visLib__chart');
- const chartType = await chart.getAttribute('data-vislib-chart-type');
+ const chartType = (await chart.getAttribute('data-vislib-chart-type')) ?? '';
if (!['line', 'area', 'histogram', 'pie'].includes(chartType)) {
this.log.debug(`-- isNewLibraryChart = false`);
diff --git a/test/functional/services/combo_box.ts b/test/functional/services/combo_box.ts
index 69794f3d2a6c8..cb9134751ae7b 100644
--- a/test/functional/services/combo_box.ts
+++ b/test/functional/services/combo_box.ts
@@ -108,7 +108,7 @@ export class ComboBoxService extends FtrService {
(
await this.find.allByCssSelector(`.euiFilterSelectItem`, this.WAIT_FOR_EXISTS_TIME)
).map(async (e) => {
- const title = await e.getAttribute('title');
+ const title = (await e.getAttribute('title')) ?? '';
return { title, formattedTitle: title.toLowerCase().trim() };
})
)
diff --git a/test/functional/services/dashboard/panel_drilldown_actions.ts b/test/functional/services/dashboard/panel_drilldown_actions.ts
index 5b76ce62dd02d..57fc42f323fbe 100644
--- a/test/functional/services/dashboard/panel_drilldown_actions.ts
+++ b/test/functional/services/dashboard/panel_drilldown_actions.ts
@@ -62,7 +62,7 @@ export function DashboardDrilldownPanelActionsProvider({ getService }: FtrProvid
async getActionHrefByText(text: string) {
log.debug(`getActionHref: "${text}"`);
const item = await this.getActionWebElementByText(text);
- return item.getAttribute('href');
+ return (await item.getAttribute('href')) ?? '';
}
async openHrefByText(text: string) {
diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts
index 5866c0ee3f18a..be4abf4e0daf2 100644
--- a/test/functional/services/filter_bar.ts
+++ b/test/functional/services/filter_bar.ts
@@ -175,12 +175,12 @@ export class FilterBarService extends FtrService {
public async isFilterPinned(key: string): Promise {
const filter = await this.testSubjects.find(`~filter & ~filter-key-${key}`);
- return (await filter.getAttribute('data-test-subj')).includes('filter-pinned');
+ return ((await filter.getAttribute('data-test-subj')) ?? '').includes('filter-pinned');
}
public async isFilterNegated(key: string): Promise {
const filter = await this.testSubjects.find(`~filter & ~filter-key-${key}`);
- return (await filter.getAttribute('data-test-subj')).includes('filter-negated');
+ return ((await filter.getAttribute('data-test-subj')) ?? '').includes('filter-negated');
}
public async getFilterCount(): Promise {
diff --git a/test/functional/services/global_nav.ts b/test/functional/services/global_nav.ts
index 4bbea9c9fd7a7..bac2d76a36bbb 100644
--- a/test/functional/services/global_nav.ts
+++ b/test/functional/services/global_nav.ts
@@ -42,10 +42,8 @@ export class GlobalNavService extends FtrService {
public async badgeExistsOrFail(expectedLabel: string): Promise {
await this.testSubjects.existOrFail('headerBadge');
- const actualLabel = await this.testSubjects.getAttribute(
- 'headerBadge',
- 'data-test-badge-label'
- );
+ const actualLabel =
+ (await this.testSubjects.getAttribute('headerBadge', 'data-test-badge-label')) ?? '';
expect(actualLabel.toUpperCase()).to.equal(expectedLabel.toUpperCase());
}
diff --git a/test/functional/services/query_bar.ts b/test/functional/services/query_bar.ts
index ca6c161accc39..fa50432be10c5 100644
--- a/test/functional/services/query_bar.ts
+++ b/test/functional/services/query_bar.ts
@@ -18,7 +18,7 @@ export class QueryBarService extends FtrService {
private readonly find = this.ctx.getService('find');
async getQueryString(): Promise {
- return await this.testSubjects.getAttribute('queryInput', 'value');
+ return (await this.testSubjects.getAttribute('queryInput', 'value')) ?? '';
}
public async setQuery(query: string): Promise {
diff --git a/test/functional/services/selectable.ts b/test/functional/services/selectable.ts
index 8dd347ace74af..ce36da946afb3 100644
--- a/test/functional/services/selectable.ts
+++ b/test/functional/services/selectable.ts
@@ -64,7 +64,7 @@ export class SelectableService extends FtrService {
const textWrapper = await option.findByClassName('euiSelectableListItem__text');
// Use innerText as getVisibleText doesn't return deeply nested text
- const innerText = await textWrapper.getAttribute('innerText');
+ const innerText = (await textWrapper.getAttribute('innerText')) ?? '';
// Replace screen reader and other Eui related text
const visibleText = innerText
diff --git a/test/functional/services/visualizations/elastic_chart.ts b/test/functional/services/visualizations/elastic_chart.ts
index c3f2fdb20f388..d3ec3052cb4fa 100644
--- a/test/functional/services/visualizations/elastic_chart.ts
+++ b/test/functional/services/visualizations/elastic_chart.ts
@@ -124,12 +124,10 @@ export class ElasticChartService extends FtrService {
*/
public async getChartDebugDataFromChart(chart: WebElementWrapper): Promise {
const visContainer = await chart.findByCssSelector('.echChartStatus');
- const debugDataString: string | undefined = await visContainer.getAttribute(
- 'data-ech-debug-state'
- );
+ const debugDataString = await visContainer.getAttribute('data-ech-debug-state');
this.log.debug('data-ech-debug-state: ', debugDataString);
- if (debugDataString === undefined) {
+ if (!debugDataString) {
throw Error(
`Elastic charts debugState not found, ensure 'setNewChartUiDebugFlag' is called before DOM rendering starts.`
);
diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts
index 3c685222b143f..0c1254bf7a24a 100644
--- a/test/functional/services/visualizations/pie_chart.ts
+++ b/test/functional/services/visualizations/pie_chart.ts
@@ -126,7 +126,7 @@ export class PieChartService extends FtrService {
}
const pieSlices = await this.getAllPieSlices(name);
const slicesStyles = await Promise.all(
- pieSlices.map(async (pieSlice) => await pieSlice.getAttribute('style'))
+ pieSlices.map(async (pieSlice) => (await pieSlice.getAttribute('style')) ?? '')
);
return slicesStyles
.map(
diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts
index ad8816989f133..038d16a82ca82 100644
--- a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts
+++ b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts
@@ -107,7 +107,7 @@ export function AddCisIntegrationFormPageProvider({
const integrationList = await testSubjects.findAll('agentEnrollmentFlyout');
await integrationList[0].click();
await PageObjects.header.waitUntilLoadingHasFinished();
- const fieldValue = await (await testSubjects.find(field)).getAttribute(value);
+ const fieldValue = (await (await testSubjects.find(field)).getAttribute(value)) ?? '';
return fieldValue;
},
};
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 f797e3503a092..bc1ae63cea51e 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
@@ -134,9 +134,11 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider
async getColumnIndex(columnName: string) {
const element = await this.getElement();
- const columnIndex = await (
+ const columnIndexAttr = await (
await element.findByCssSelector(`[data-gridcell-column-id="${columnName}"]`)
).getAttribute('data-gridcell-column-index');
+ expect(columnIndexAttr).to.not.be(null);
+ const columnIndex = parseInt(columnIndexAttr ?? '-1', 10);
expect(columnIndex).to.be.greaterThan(-1);
return columnIndex;
},
diff --git a/x-pack/test/custom_branding/tests/settings.ts b/x-pack/test/custom_branding/tests/settings.ts
index 234aedeb11c2a..df4ccc53e7893 100644
--- a/x-pack/test/custom_branding/tests/settings.ts
+++ b/x-pack/test/custom_branding/tests/settings.ts
@@ -80,7 +80,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await goToSettings();
const img = await find.byCssSelector('img[alt="logo"]');
- const imgSrc = await img.getAttribute('src');
+ const imgSrc = (await img.getAttribute('src')) ?? '';
expect(imgSrc.startsWith('data:image/png')).to.be(true);
});
@@ -93,7 +93,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await goToSettings();
const logo = await testSubjects.find('logo');
const img = await logo.findByCssSelector('.chrHeaderLogo__mark');
- const imgSrc = await img.getAttribute('src');
+ const imgSrc = (await img.getAttribute('src')) ?? '';
expect(imgSrc.startsWith('data:image/png')).to.be(true);
});
});
diff --git a/x-pack/test/examples/search_examples/partial_results_example.ts b/x-pack/test/examples/search_examples/partial_results_example.ts
index 269b2e79ab38f..4548ab7c191dc 100644
--- a/x-pack/test/examples/search_examples/partial_results_example.ts
+++ b/x-pack/test/examples/search_examples/partial_results_example.ts
@@ -30,7 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('requestFibonacci');
await retry.waitFor('update progress bar', async () => {
- const newValue = await progressBar.getAttribute('value');
+ const newValue = (await progressBar.getAttribute('value')) ?? '';
return parseFloat(newValue) > 0;
});
});
diff --git a/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_chart_action.ts b/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_chart_action.ts
index b8521914da174..7491acc447979 100644
--- a/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_chart_action.ts
+++ b/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_chart_action.ts
@@ -39,7 +39,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('action is a link element', async () => {
const actionElement = await testSubjects.find(ACTION_TEST_SUBJ);
const tag = await actionElement.getTagName();
- const href = await actionElement.getAttribute('href');
+ const href = (await actionElement.getAttribute('href')) ?? '';
expect(tag.toLowerCase()).to.be('a');
expect(typeof href).to.be('string');
diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts
index 78166fd85d22e..50c1a7f860072 100644
--- a/x-pack/test/functional/apps/discover/reporting.ts
+++ b/x-pack/test/functional/apps/discover/reporting.ts
@@ -125,7 +125,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await browser.getActions().keyDown(Key.CONTROL).perform();
await browser.getActions().keyDown('v').perform();
- const reportURL = decodeURIComponent(await textInput.getAttribute('value'));
+ const reportURL = decodeURIComponent((await textInput.getAttribute('value')) ?? '');
// get number of filters in URLs
const timeFiltersNumberInReportURL =
diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts
index 2c779555d6227..5d0fd7cd14947 100644
--- a/x-pack/test/functional/page_objects/gis_page.ts
+++ b/x-pack/test/functional/page_objects/gis_page.ts
@@ -310,9 +310,9 @@ export class GisPageObject extends FtrService {
await this.setViewPopoverToggle.close();
return {
- lat: parseFloat(lat),
- lon: parseFloat(lon),
- zoom: parseFloat(zoom),
+ lat: parseFloat(lat ?? ''),
+ lon: parseFloat(lon ?? ''),
+ zoom: parseFloat(zoom ?? ''),
};
}
diff --git a/x-pack/test/functional/page_objects/graph_page.ts b/x-pack/test/functional/page_objects/graph_page.ts
index 810048a7f144f..c4e2c8010c31b 100644
--- a/x-pack/test/functional/page_objects/graph_page.ts
+++ b/x-pack/test/functional/page_objects/graph_page.ts
@@ -76,16 +76,16 @@ export class GraphPageObject extends FtrService {
}
private async getCirclePosition(element: WebElementWrapper) {
- const x = await element.getAttribute('cx');
- const y = await element.getAttribute('cy');
+ const x = (await element.getAttribute('cx')) ?? '';
+ const y = (await element.getAttribute('cy')) ?? '';
return this.getPositionAsString(x, y);
}
private async getLinePositions(element: WebElementWrapper) {
- const x1 = await element.getAttribute('x1');
- const y1 = await element.getAttribute('y1');
- const x2 = await element.getAttribute('x2');
- const y2 = await element.getAttribute('y2');
+ const x1 = (await element.getAttribute('x1')) ?? '';
+ const y1 = (await element.getAttribute('y1')) ?? '';
+ const x2 = (await element.getAttribute('x2')) ?? '';
+ const y2 = (await element.getAttribute('y2')) ?? '';
return [this.getPositionAsString(x1, y1), this.getPositionAsString(x2, y2)];
}
@@ -154,7 +154,7 @@ export class GraphPageObject extends FtrService {
const tagName: string = await element.getTagName();
if (tagName === 'line') {
const [sourcePosition, targetPosition] = await this.getLinePositions(element);
- const lineStyle = await element.getAttribute('style');
+ const lineStyle = (await element.getAttribute('style')) ?? '';
// grep out the width of the connection from the style attribute
const strokeWidth = Number(/stroke-width: ?(\d+(\.\d+)?)/.exec(lineStyle)![1]);
edges.push({
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index c82ac1a72ec81..af877fb24e07b 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -1126,7 +1126,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
async getDatatableCellStyle(rowIndex = 0, colIndex = 0) {
const el = await this.getDatatableCell(rowIndex, colIndex);
- const styleString = await el.getAttribute('style');
+ const styleString = (await el.getAttribute('style')) ?? '';
return styleString.split(';').reduce>((memo, cssLine) => {
const [prop, value] = cssLine.split(':');
if (prop && value) {
@@ -1138,7 +1138,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
async getDatatableCellSpanStyle(rowIndex = 0, colIndex = 0) {
const el = await (await this.getDatatableCell(rowIndex, colIndex)).findByCssSelector('span');
- const styleString = await el.getAttribute('style');
+ const styleString = (await el.getAttribute('style')) ?? '';
return styleString.split(';').reduce>((memo, cssLine) => {
const [prop, value] = cssLine.split(':');
if (prop && value) {
@@ -1317,7 +1317,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
async getLegacyMetricStyle() {
const el = await testSubjects.find('metric_value');
- const styleString = await el.getAttribute('style');
+ const styleString = (await el.getAttribute('style')) ?? '';
return styleString.split(';').reduce>((memo, cssLine) => {
const [prop, value] = cssLine.split(':');
if (prop && value) {
@@ -1792,7 +1792,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
return Promise.all(
allFieldsForType.map(async (el) => {
const parent = await el.findByXpath('./..');
- return parent.getAttribute('data-test-subj');
+ return (await parent.getAttribute('data-test-subj')) ?? '';
})
);
},
@@ -1857,6 +1857,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await testSubjects.click(testSubFrom);
const copyButton = await testSubjects.find('copyShareUrlButton');
const url = await copyButton.getAttribute('data-share-url');
+ if (!url) {
+ throw Error('No data-share-url attribute found');
+ }
return url;
},
diff --git a/x-pack/test/functional/page_objects/reporting_page.ts b/x-pack/test/functional/page_objects/reporting_page.ts
index 789421f1985d1..052d1898e4485 100644
--- a/x-pack/test/functional/page_objects/reporting_page.ts
+++ b/x-pack/test/functional/page_objects/reporting_page.ts
@@ -46,6 +46,14 @@ export class ReportingPageObject extends FtrService {
'href',
timeout
);
+ if (!url) {
+ throw new Error(
+ `${
+ url === null ? 'No' : 'Empty'
+ } href found on [data-test-subj="downloadCompletedReportButton"]`
+ );
+ }
+
this.log.debug(`getReportURL got url: ${url}`);
return url;
diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts
index 968a88e3cad6f..6d704387e21f2 100644
--- a/x-pack/test/functional/page_objects/search_sessions_management_page.ts
+++ b/x-pack/test/functional/page_objects/search_sessions_management_page.ts
@@ -34,7 +34,7 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr
const actionsCell = await row.findByTestSubject('sessionManagementActionsCol');
return {
- id: (await row.getAttribute('data-test-search-session-id')).split('id-')[1],
+ id: ((await row.getAttribute('data-test-search-session-id')) ?? '').split('id-')[1],
name: $.findTestSubject('sessionManagementNameCol').text().trim(),
status: $.findTestSubject('sessionManagementStatusLabel').attr('data-test-status'),
mainUrl: $.findTestSubject('sessionManagementNameCol').text(),
diff --git a/x-pack/test/functional/page_objects/tag_management_page.ts b/x-pack/test/functional/page_objects/tag_management_page.ts
index 4684ad3e6a70e..68100ef3ad6db 100644
--- a/x-pack/test/functional/page_objects/tag_management_page.ts
+++ b/x-pack/test/functional/page_objects/tag_management_page.ts
@@ -95,9 +95,10 @@ class TagModal extends FtrService {
*/
async getFormValues(): Promise> {
return {
- name: await this.testSubjects.getAttribute('createModalField-name', 'value'),
- color: await this.testSubjects.getAttribute('~createModalField-color', 'value'),
- description: await this.testSubjects.getAttribute('createModalField-description', 'value'),
+ name: (await this.testSubjects.getAttribute('createModalField-name', 'value')) ?? '',
+ color: (await this.testSubjects.getAttribute('~createModalField-color', 'value')) ?? '',
+ description:
+ (await this.testSubjects.getAttribute('createModalField-description', 'value')) ?? '',
};
}
diff --git a/x-pack/test/functional/page_objects/watcher_page.ts b/x-pack/test/functional/page_objects/watcher_page.ts
index ad5153a933466..8eb50ffe203e3 100644
--- a/x-pack/test/functional/page_objects/watcher_page.ts
+++ b/x-pack/test/functional/page_objects/watcher_page.ts
@@ -57,7 +57,7 @@ export class WatcherPageObject extends FtrService {
const name = await watch.findByCssSelector('td:nth-child(3)');
return {
- checkBox: (await checkBox.getAttribute('innerHTML')).includes('input'),
+ checkBox: ((await checkBox.getAttribute('innerHTML')) ?? '').includes('input'),
id: await id.getVisibleText(),
name: (await name.getVisibleText()).split(',').map((role) => role.trim()),
};
diff --git a/x-pack/test/functional/services/ml/anomalies_table.ts b/x-pack/test/functional/services/ml/anomalies_table.ts
index c59221289f848..35a53d90f6b9c 100644
--- a/x-pack/test/functional/services/ml/anomalies_table.ts
+++ b/x-pack/test/functional/services/ml/anomalies_table.ts
@@ -219,7 +219,7 @@ export function MachineLearningAnomaliesTableProvider({ getService }: FtrProvide
},
async scrollRowIntoView(rowIndex: number) {
- const rowSubj = await this.getRowSubjByRowIndex(rowIndex);
+ const rowSubj = (await this.getRowSubjByRowIndex(rowIndex)) ?? '';
await testSubjects.scrollIntoView(rowSubj);
},
};
diff --git a/x-pack/test/functional/services/ml/common_table_service.ts b/x-pack/test/functional/services/ml/common_table_service.ts
index a6435ab4b668d..3e8227323d763 100644
--- a/x-pack/test/functional/services/ml/common_table_service.ts
+++ b/x-pack/test/functional/services/ml/common_table_service.ts
@@ -128,8 +128,10 @@ export function MlTableServiceProvider({ getPageObject, getService }: FtrProvide
const headers = await table.findAllByClassName('euiTableHeaderCell');
for (const header of headers) {
const ariaSort = await header.getAttribute('aria-sort');
- if (ariaSort !== 'none') {
- const columnNameFragments = (await header.getAttribute('data-test-subj')).split('_');
+ if (ariaSort && ariaSort !== 'none') {
+ const columnNameFragments = ((await header.getAttribute('data-test-subj')) ?? '').split(
+ '_'
+ );
const columnName = columnNameFragments.slice(1, columnNameFragments.length - 1).join('_');
return { columnName, direction: ariaSort.replace('ending', '') };
}
diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts
index 98404c88b412e..9183cf42ab041 100644
--- a/x-pack/test/functional/services/ml/common_ui.ts
+++ b/x-pack/test/functional/services/ml/common_ui.ts
@@ -216,7 +216,7 @@ export function MachineLearningCommonUIProvider({
const slider = await testSubjects.find(testDataSubj);
await retry.tryForTime(60 * 1000, async () => {
- const currentValue = await slider.getAttribute('value');
+ const currentValue = (await slider.getAttribute('value')) ?? '';
const currentDiff = +currentValue - +value;
if (currentDiff === 0) {
diff --git a/x-pack/test/functional/services/ml/job_wizard_advanced.ts b/x-pack/test/functional/services/ml/job_wizard_advanced.ts
index 8679a61be31f1..317001efd75f8 100644
--- a/x-pack/test/functional/services/ml/job_wizard_advanced.ts
+++ b/x-pack/test/functional/services/ml/job_wizard_advanced.ts
@@ -22,10 +22,10 @@ export function MachineLearningJobWizardAdvancedProvider(
return {
async getValueOrPlaceholder(inputLocator: string): Promise {
const value = await testSubjects.getAttribute(inputLocator, 'value');
- if (value !== '') {
- return value;
+ if (!value) {
+ return (await testSubjects.getAttribute(inputLocator, 'placeholder')) ?? '';
} else {
- return await testSubjects.getAttribute(inputLocator, 'placeholder');
+ return value;
}
},
diff --git a/x-pack/test/functional/services/ml/stack_management_jobs.ts b/x-pack/test/functional/services/ml/stack_management_jobs.ts
index b67e90f126a61..63c2005650c0d 100644
--- a/x-pack/test/functional/services/ml/stack_management_jobs.ts
+++ b/x-pack/test/functional/services/ml/stack_management_jobs.ts
@@ -477,7 +477,7 @@ export function MachineLearningStackManagementJobsProvider({
).findAllByClassName('euiAvatar--space');
for (const el of spacesEl) {
- spaces.push((await el.getAttribute('data-test-subj')).replace('space-avatar-', ''));
+ spaces.push(((await el.getAttribute('data-test-subj')) ?? '').replace('space-avatar-', ''));
}
return spaces;
diff --git a/x-pack/test/functional/services/uptime/settings.ts b/x-pack/test/functional/services/uptime/settings.ts
index e51560c813d44..84a94cd6f7243 100644
--- a/x-pack/test/functional/services/uptime/settings.ts
+++ b/x-pack/test/functional/services/uptime/settings.ts
@@ -36,9 +36,9 @@ export function UptimeSettingsProvider({ getService }: FtrProviderContext) {
const indInput = await testSubjects.find('heartbeat-indices-input-loaded', 5000);
const expirationInput = await testSubjects.find('expiration-threshold-input-loaded', 5000);
const ageInput = await testSubjects.find('age-threshold-input-loaded', 5000);
- const heartbeatIndices = await indInput.getAttribute('value');
- const expiration = await expirationInput.getAttribute('value');
- const age = await ageInput.getAttribute('value');
+ const heartbeatIndices = (await indInput.getAttribute('value')) ?? '0';
+ const expiration = (await expirationInput.getAttribute('value')) ?? '0';
+ const age = (await ageInput.getAttribute('value')) ?? '0';
return {
heartbeatIndices,
diff --git a/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts
index 008679c726ef5..4bd4f862e7d7f 100644
--- a/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts
+++ b/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts
@@ -56,7 +56,7 @@ export function RuleDetailsPageProvider({ getService }: FtrProviderContext) {
const alertDurationEpoch = await find.byCssSelector(
'input[data-test-subj="alertsDurationEpoch"]'
);
- return parseInt(await alertDurationEpoch.getAttribute('value'), 10);
+ return parseInt((await alertDurationEpoch.getAttribute('value')) ?? '0', 10);
},
async clickAlertMuteButton(alert: string) {
const muteAlertButton = await testSubjects.find(`muteAlertButton_${alert}`);
diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts
index c03728311b2a6..77c6b9b692678 100644
--- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts
+++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts
@@ -201,7 +201,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext)
await retry.tryForTime(30000, async () => {
await this.searchAlerts(ruleName);
const statusControl = await testSubjects.find(controlName);
- const title = await statusControl.getAttribute('title');
+ const title = (await statusControl.getAttribute('title')) ?? '';
expect(title.toLowerCase()).to.eql(expectedStatus.toLowerCase());
});
},
diff --git a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts
index ca202b2a9e28d..765fb2e6cdcbf 100644
--- a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts
+++ b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts
@@ -114,7 +114,8 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
describe('Feature flag', () => {
it('Link point to O11y Rules pages by default', async () => {
- const manageRulesPageHref = await observability.alerts.rulesPage.getManageRulesPageHref();
+ const manageRulesPageHref =
+ (await observability.alerts.rulesPage.getManageRulesPageHref()) ?? '';
expect(new URL(manageRulesPageHref).pathname).equal('/app/observability/alerts/rules');
});
});
diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts
index 3f586ddc89856..ae1004db12cf3 100644
--- a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts
+++ b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts
@@ -157,6 +157,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await testSubjects.find('inspectorRequestSearchSessionId')
).getAttribute('data-search-session-id');
await inspector.close();
- return searchSessionId;
+ return searchSessionId ?? '';
}
}
diff --git a/x-pack/test_serverless/functional/page_objects/svl_rule_details_ui_page.ts b/x-pack/test_serverless/functional/page_objects/svl_rule_details_ui_page.ts
index bd6ae5e0ce971..4f0e1cbe723a6 100644
--- a/x-pack/test_serverless/functional/page_objects/svl_rule_details_ui_page.ts
+++ b/x-pack/test_serverless/functional/page_objects/svl_rule_details_ui_page.ts
@@ -56,7 +56,7 @@ export function SvlRuleDetailsPageProvider({ getService }: FtrProviderContext) {
const alertDurationEpoch = await find.byCssSelector(
'input[data-test-subj="alertsDurationEpoch"]'
);
- return parseInt(await alertDurationEpoch.getAttribute('value'), 10);
+ return parseInt((await alertDurationEpoch.getAttribute('value')) ?? '', 10);
},
async clickAlertMuteButton(alert: string) {
const muteAlertButton = await testSubjects.find(`muteAlertButton_${alert}`);
diff --git a/x-pack/test_serverless/functional/page_objects/svl_triggers_actions_ui_page.ts b/x-pack/test_serverless/functional/page_objects/svl_triggers_actions_ui_page.ts
index e9f407c06753f..c084e3eda6a7b 100644
--- a/x-pack/test_serverless/functional/page_objects/svl_triggers_actions_ui_page.ts
+++ b/x-pack/test_serverless/functional/page_objects/svl_triggers_actions_ui_page.ts
@@ -202,7 +202,7 @@ export function SvlTriggersActionsPageProvider({ getService }: FtrProviderContex
await this.searchRules(ruleName);
const statusControl = await testSubjects.find(controlName);
const title = await statusControl.getAttribute('title');
- expect(title.toLowerCase()).to.eql(expectedStatus.toLowerCase());
+ expect(title?.toLowerCase()).to.eql(expectedStatus.toLowerCase());
});
},
async ensureEventLogColumnExists(columnId: string) {
diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts
index 127621ce9d0bf..eb45495bf2522 100644
--- a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts
@@ -308,10 +308,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.saveSearch(savedSearch);
await PageObjects.discover.chooseBreakdownField('extension.keyword');
await PageObjects.discover.setChartInterval('Second');
- let requestData = await testSubjects.getAttribute(
- 'unifiedHistogramChart',
- 'data-request-data'
- );
+ let requestData =
+ (await testSubjects.getAttribute('unifiedHistogramChart', 'data-request-data')) ?? '';
expect(JSON.parse(requestData)).to.eql({
dataViewId: 'long-window-logstash-*',
timeField: '@timestamp',
@@ -322,7 +320,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.revertUnsavedChanges();
await PageObjects.discover.waitUntilSearchingHasFinished();
- requestData = await testSubjects.getAttribute('unifiedHistogramChart', 'data-request-data');
+ requestData =
+ (await testSubjects.getAttribute('unifiedHistogramChart', 'data-request-data')) ?? '';
expect(JSON.parse(requestData)).to.eql({
dataViewId: 'long-window-logstash-*',
timeField: '@timestamp',
diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts
index e7e32c3c94dde..c07899a664ba4 100644
--- a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts
@@ -139,7 +139,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
.perform();
await browser.getActions().keyDown('v').perform();
- const reportURL = decodeURIComponent(await textInput.getAttribute('value'));
+ const reportURL = decodeURIComponent((await textInput.getAttribute('value')) ?? '');
// get number of filters in URLs
const timeFiltersNumberInReportURL =
diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/partial_results_example.ts b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/partial_results_example.ts
index 6e8fbb465bb08..727551308b0b9 100644
--- a/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/partial_results_example.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/partial_results_example.ts
@@ -30,7 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('requestFibonacci');
await retry.waitFor('update progress bar', async () => {
- const newValue = await progressBar.getAttribute('value');
+ const newValue = (await progressBar.getAttribute('value')) ?? '';
return parseFloat(newValue) > 0;
});
});
From a2e9e093c81690acdd378e2c14aa006b3079ade2 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Wed, 10 Apr 2024 18:36:58 +0100
Subject: [PATCH 27/55] skip flaky suite (#180496)
---
.../api_integration/apis/transform/schedule_now_transforms.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/api_integration/apis/transform/schedule_now_transforms.ts b/x-pack/test/api_integration/apis/transform/schedule_now_transforms.ts
index bca14394e6886..c5cc2c700d702 100644
--- a/x-pack/test/api_integration/apis/transform/schedule_now_transforms.ts
+++ b/x-pack/test/api_integration/apis/transform/schedule_now_transforms.ts
@@ -98,7 +98,8 @@ export default ({ getService }: FtrProviderContext) => {
});
});
- describe('bulk schedule', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/180496
+ describe.skip('bulk schedule', function () {
const reqBody: ScheduleNowTransformsRequestSchema = [
{ id: 'bulk_schedule_now_test_1' },
{ id: 'bulk_schedule_now_test_2' },
From 54066e0cda1c70b7c01024a9c68bccb40515cc6a Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Wed, 10 Apr 2024 18:39:30 +0100
Subject: [PATCH 28/55] skip flaky suite (#180497)
---
.../api_integration/apis/transform/schedule_now_transforms.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/x-pack/test/api_integration/apis/transform/schedule_now_transforms.ts b/x-pack/test/api_integration/apis/transform/schedule_now_transforms.ts
index c5cc2c700d702..f2a420640ba88 100644
--- a/x-pack/test/api_integration/apis/transform/schedule_now_transforms.ts
+++ b/x-pack/test/api_integration/apis/transform/schedule_now_transforms.ts
@@ -99,6 +99,7 @@ export default ({ getService }: FtrProviderContext) => {
});
// FLAKY: https://github.com/elastic/kibana/issues/180496
+ // FLAKY: https://github.com/elastic/kibana/issues/180497
describe.skip('bulk schedule', function () {
const reqBody: ScheduleNowTransformsRequestSchema = [
{ id: 'bulk_schedule_now_test_1' },
From ebc060662b71f025542be0ac7a6fe8d63fb64b03 Mon Sep 17 00:00:00 2001
From: Lukas Olson
Date: Wed, 10 Apr 2024 19:45:55 +0200
Subject: [PATCH 29/55] [data.search] Poll on async status (#178921)
## Summary
Resolves https://github.com/elastic/kibana/issues/174218.
This PR updates the default (`ese`) search strategy to poll on the [get
async
status](https://www.elastic.co/guide/en/elasticsearch/reference/8.12/async-search.html#get-async-search-status)
endpoint rather than the [get async
search](https://www.elastic.co/guide/en/elasticsearch/reference/8.12/async-search.html#get-async-search)
endpoint. This takes advantage of optimizations in ES to increase
performance of async search.
### Release notes
Kibana now uses Elasticsearch's `_async_search/status/{id}` endpoint
(instead of `_async_search/{id}`) when polling on search requests to
improve performance.
### Checklist
- [ ]
[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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
### Risk Matrix
| Risk | Probability | Severity | Mitigation/Notes |
|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces—unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes—Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../search/session/get_search_status.ts | 3 +-
.../ese_search/ese_search_strategy.test.ts | 44 +++++++
.../ese_search/ese_search_strategy.ts | 115 ++++++++++++------
.../strategies/ese_search/response_utils.ts | 22 +++-
.../search/strategies/ese_search/types.ts | 2 +-
5 files changed, 142 insertions(+), 44 deletions(-)
diff --git a/src/plugins/data/server/search/session/get_search_status.ts b/src/plugins/data/server/search/session/get_search_status.ts
index 46637555fe5e2..241314862ad0a 100644
--- a/src/plugins/data/server/search/session/get_search_status.ts
+++ b/src/plugins/data/server/search/session/get_search_status.ts
@@ -20,7 +20,6 @@ export async function getSearchStatus(
// TODO: Handle strategies other than the default one
// https://github.com/elastic/kibana/issues/127880
try {
- // @ts-expect-error start_time_in_millis: EpochMillis is string | number
const apiResponse: TransportResult =
await internalClient.asyncSearch.status(
{
@@ -29,7 +28,7 @@ export async function getSearchStatus(
{ meta: true }
);
const response = apiResponse.body;
- if (response.completion_status >= 400) {
+ if (response.completion_status! >= 400) {
return {
status: SearchStatus.ERROR,
error: i18n.translate('data.search.statusError', {
diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts
index 0e955597e6c25..b50ccc508dbfd 100644
--- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts
+++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts
@@ -18,6 +18,22 @@ import { createSearchSessionsClientMock } from '../../mocks';
import { getMockSearchConfig } from '../../../../config.mock';
import { DataViewType } from '@kbn/data-views-plugin/common';
+const mockAsyncStatusResponse = (isComplete = false) => ({
+ body: {
+ id: 'FlVYVkw0clJIUS1TMHpHdXA3a29pZUEedldKX1c1bnBRVXFmalZ4emV1cjFCUToxNjYzMDgx',
+ is_running: !isComplete,
+ is_partial: !isComplete,
+ start_time_in_millis: 1710451842532,
+ expiration_time_in_millis: 1710451907469,
+ _shards: {
+ total: 10,
+ successful: 0,
+ skipped: 0,
+ failed: 0,
+ },
+ },
+});
+
const mockAsyncResponse = {
body: {
id: 'foo',
@@ -45,6 +61,7 @@ const mockRollupResponse = {
describe('ES search strategy', () => {
const mockApiCaller = jest.fn();
+ const mockStatusCaller = jest.fn();
const mockGetCaller = jest.fn();
const mockSubmitCaller = jest.fn();
const mockDeleteCaller = jest.fn();
@@ -58,6 +75,7 @@ describe('ES search strategy', () => {
esClient: {
asCurrentUser: {
asyncSearch: {
+ status: mockStatusCaller,
get: mockGetCaller,
submit: mockSubmitCaller,
delete: mockDeleteCaller,
@@ -82,6 +100,7 @@ describe('ES search strategy', () => {
beforeEach(() => {
mockApiCaller.mockClear();
+ mockStatusCaller.mockClear();
mockGetCaller.mockClear();
mockSubmitCaller.mockClear();
mockDeleteCaller.mockClear();
@@ -118,7 +137,26 @@ describe('ES search strategy', () => {
expect(request).toHaveProperty('keep_alive', '60000ms');
});
+ it('returns status if incomplete', async () => {
+ mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(false));
+
+ const params = { index: 'logstash-*', body: { query: {} } };
+ const esSearch = await enhancedEsSearchStrategyProvider(
+ mockLegacyConfig$,
+ mockSearchConfig,
+ mockLogger
+ );
+
+ const response = await firstValueFrom(esSearch.search({ id: 'foo', params }, {}, mockDeps));
+
+ expect(mockGetCaller).not.toBeCalled();
+ expect(response).toHaveProperty('id');
+ expect(response).toHaveProperty('isPartial', true);
+ expect(response).toHaveProperty('isRunning', true);
+ });
+
it('makes a GET request to async search with ID', async () => {
+ mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true));
mockGetCaller.mockResolvedValueOnce(mockAsyncResponse);
const params = { index: 'logstash-*', body: { query: {} } };
@@ -138,6 +176,7 @@ describe('ES search strategy', () => {
});
it('allows overriding keep_alive and wait_for_completion_timeout', async () => {
+ mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true));
mockGetCaller.mockResolvedValueOnce(mockAsyncResponse);
const params = {
@@ -193,6 +232,7 @@ describe('ES search strategy', () => {
});
it('sets transport options on GET requests', async () => {
+ mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true));
mockGetCaller.mockResolvedValueOnce(mockAsyncResponse);
const params = { index: 'logstash-*', body: { query: {} } };
const esSearch = enhancedEsSearchStrategyProvider(
@@ -361,6 +401,7 @@ describe('ES search strategy', () => {
});
it('makes a GET request to async search with short keepalive, if session is not saved', async () => {
+ mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true));
mockGetCaller.mockResolvedValueOnce(mockAsyncResponse);
const params = { index: 'logstash-*', body: { query: {} } };
@@ -380,6 +421,7 @@ describe('ES search strategy', () => {
});
it('makes a GET request to async search with long keepalive, if session is saved', async () => {
+ mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true));
mockGetCaller.mockResolvedValueOnce(mockAsyncResponse);
const params = { index: 'logstash-*', body: { query: {} } };
@@ -401,6 +443,7 @@ describe('ES search strategy', () => {
});
it('makes a GET request to async search with no keepalive, if session is session saved and search is stored', async () => {
+ mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true));
mockGetCaller.mockResolvedValueOnce(mockAsyncResponse);
const params = { index: 'logstash-*', body: { query: {} } };
@@ -592,6 +635,7 @@ describe('ES search strategy', () => {
it('throws normalized error on ElasticsearchClientError', async () => {
const errResponse = new errors.ElasticsearchClientError('something is wrong with EsClient');
+ mockStatusCaller.mockResolvedValueOnce(mockAsyncStatusResponse(true));
mockGetCaller.mockRejectedValue(errResponse);
const id = 'some_other_id';
diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts
index 42c74693b3299..94276ba639d7f 100644
--- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts
+++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts
@@ -7,7 +7,7 @@
*/
import type { Observable } from 'rxjs';
-import type { IScopedClusterClient, Logger, SharedGlobalConfig } from '@kbn/core/server';
+import type { Logger, SharedGlobalConfig } from '@kbn/core/server';
import { catchError, tap } from 'rxjs';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { firstValueFrom, from } from 'rxjs';
@@ -21,13 +21,13 @@ import type {
IEsSearchResponse,
ISearchOptions,
} from '../../../../common';
-import { DataViewType, pollSearch } from '../../../../common';
+import { DataViewType, isRunningResponse, pollSearch } from '../../../../common';
import {
getDefaultAsyncGetParams,
getDefaultAsyncSubmitParams,
getIgnoreThrottled,
} from './request_utils';
-import { toAsyncKibanaSearchResponse } from './response_utils';
+import { toAsyncKibanaSearchResponse, toAsyncKibanaSearchStatusResponse } from './response_utils';
import { SearchUsage, searchUsageObserver } from '../../collectors/search';
import {
getDefaultSearchParams,
@@ -45,56 +45,93 @@ export const enhancedEsSearchStrategyProvider = (
usage?: SearchUsage,
useInternalUser: boolean = false
): ISearchStrategy> => {
- function cancelAsyncSearch(id: string, esClient: IScopedClusterClient) {
+ function cancelAsyncSearch(id: string, { esClient }: SearchStrategyDependencies) {
const client = useInternalUser ? esClient.asInternalUser : esClient.asCurrentUser;
return client.asyncSearch.delete({ id });
}
- function asyncSearch(
+ async function asyncSearchStatus(
{ id, ...request }: IEsSearchRequest,
options: IAsyncSearchOptions,
+ { esClient }: Pick
+ ) {
+ const client = useInternalUser ? esClient.asInternalUser : esClient.asCurrentUser;
+ const keepAlive =
+ request.params?.keep_alive ?? getDefaultAsyncGetParams(searchConfig, options).keep_alive;
+
+ const { body, headers } = await client.asyncSearch.status(
+ // @ts-expect-error keep_alive was recently added and the types haven't been updated yet
+ { id: id!, keep_alive: keepAlive },
+ { ...options.transport, signal: options.abortSignal, meta: true }
+ );
+ return toAsyncKibanaSearchStatusResponse(body, headers?.warning);
+ }
+
+ // Gets the current status of the async search request. If the request is complete, then queries for the results.
+ async function getAsyncSearch(
+ { id, ...request }: IEsSearchRequest,
+ options: IAsyncSearchOptions,
+ { esClient }: SearchStrategyDependencies
+ ) {
+ // First, request the status of the async search, and return the status if incomplete
+ const status = await asyncSearchStatus({ id, ...request }, options, { esClient });
+ if (isRunningResponse(status)) return status;
+
+ // Then, if the search is complete, request & return the final results
+ const client = useInternalUser ? esClient.asInternalUser : esClient.asCurrentUser;
+ const params = {
+ ...getDefaultAsyncGetParams(searchConfig, options),
+ ...(request.params?.keep_alive ? { keep_alive: request.params.keep_alive } : {}),
+ ...(request.params?.wait_for_completion_timeout
+ ? { wait_for_completion_timeout: request.params.wait_for_completion_timeout }
+ : {}),
+ };
+ const { body, headers } = await client.asyncSearch.get(
+ { ...params, id: id! },
+ { ...options.transport, signal: options.abortSignal, meta: true }
+ );
+ const response = shimHitsTotal(body.response, options);
+ return toAsyncKibanaSearchResponse({ ...body, response }, headers?.warning);
+ }
+
+ async function submitAsyncSearch(
+ request: IEsSearchRequest,
+ options: IAsyncSearchOptions,
{ esClient, uiSettingsClient }: SearchStrategyDependencies
) {
const client = useInternalUser ? esClient.asInternalUser : esClient.asCurrentUser;
+ const params = {
+ ...(await getDefaultAsyncSubmitParams(uiSettingsClient, searchConfig, options)),
+ ...request.params,
+ };
+ const { body, headers, meta } = await client.asyncSearch.submit(params, {
+ ...options.transport,
+ signal: options.abortSignal,
+ meta: true,
+ });
+ const response = shimHitsTotal(body.response, options);
+ return toAsyncKibanaSearchResponse(
+ { ...body, response },
+ headers?.warning,
+ meta?.request?.params
+ );
+ }
+ function asyncSearch(
+ { id, ...request }: IEsSearchRequest,
+ options: IAsyncSearchOptions,
+ deps: SearchStrategyDependencies
+ ) {
const search = async () => {
- const params = id
- ? {
- ...getDefaultAsyncGetParams(searchConfig, options),
- ...(request.params?.keep_alive ? { keep_alive: request.params.keep_alive } : {}),
- ...(request.params?.wait_for_completion_timeout
- ? { wait_for_completion_timeout: request.params.wait_for_completion_timeout }
- : {}),
- }
- : {
- ...(await getDefaultAsyncSubmitParams(uiSettingsClient, searchConfig, options)),
- ...request.params,
- };
- const { body, headers, meta } = id
- ? await client.asyncSearch.get(
- { ...params, id },
- { ...options.transport, signal: options.abortSignal, meta: true }
- )
- : await client.asyncSearch.submit(params, {
- ...options.transport,
- signal: options.abortSignal,
- meta: true,
- });
-
- const response = shimHitsTotal(body.response, options);
-
- return toAsyncKibanaSearchResponse(
- { ...body, response },
- headers?.warning,
- // do not return requestParams on polling calls
- id ? undefined : meta?.request?.params
- );
+ return id
+ ? await getAsyncSearch({ id, ...request }, options, deps)
+ : await submitAsyncSearch(request, options, deps);
};
const cancel = async () => {
if (!id || options.isStored) return;
try {
- await cancelAsyncSearch(id, esClient);
+ await cancelAsyncSearch(id, deps);
} catch (e) {
// A 404 means either this search request does not exist, or that it is already cancelled
if (e.meta?.statusCode === 404) return;
@@ -184,10 +221,10 @@ export const enhancedEsSearchStrategyProvider = (
* @returns `Promise`
* @throws `KbnServerError`
*/
- cancel: async (id, options, { esClient }) => {
+ cancel: async (id, options, deps) => {
logger.debug(`cancel ${id}`);
try {
- await cancelAsyncSearch(id, esClient);
+ await cancelAsyncSearch(id, deps);
} catch (e) {
throw getKbnServerError(e);
}
diff --git a/src/plugins/data/server/search/strategies/ese_search/response_utils.ts b/src/plugins/data/server/search/strategies/ese_search/response_utils.ts
index c9cb4acf3f3a9..290f44b1b4f9b 100644
--- a/src/plugins/data/server/search/strategies/ese_search/response_utils.ts
+++ b/src/plugins/data/server/search/strategies/ese_search/response_utils.ts
@@ -7,18 +7,36 @@
*/
import type { ConnectionRequestParams } from '@elastic/transport';
+import { IKibanaSearchResponse } from '../../../../common';
import type { AsyncSearchResponse } from './types';
import { getTotalLoaded } from '../es_search';
import { sanitizeRequestParams } from '../../sanitize_request_params';
+import { AsyncSearchStatusResponse } from './types';
/**
- * Get the Kibana representation of an async search response (see `IKibanaSearchResponse`).
+ * Get the Kibana representation of an async search status response.
+ */
+export function toAsyncKibanaSearchStatusResponse(
+ response: AsyncSearchStatusResponse,
+ warning?: string
+): IKibanaSearchResponse {
+ return {
+ id: response.id,
+ rawResponse: {},
+ isPartial: response.is_partial,
+ isRunning: response.is_running,
+ ...(warning ? { warning } : {}),
+ };
+}
+
+/**
+ * Get the Kibana representation of an async search response.
*/
export function toAsyncKibanaSearchResponse(
response: AsyncSearchResponse,
warning?: string,
requestParams?: ConnectionRequestParams
-) {
+): IKibanaSearchResponse {
return {
id: response.id,
rawResponse: response.response,
diff --git a/src/plugins/data/server/search/strategies/ese_search/types.ts b/src/plugins/data/server/search/strategies/ese_search/types.ts
index 5ff324e1c2e4f..443ec6039a628 100644
--- a/src/plugins/data/server/search/strategies/ese_search/types.ts
+++ b/src/plugins/data/server/search/strategies/ese_search/types.ts
@@ -27,6 +27,6 @@ export interface AsyncSearchResponse {
is_running: boolean;
}
export interface AsyncSearchStatusResponse extends Omit {
- completion_status: number;
+ completion_status?: number;
_shards: ShardStatistics;
}
From f018af0d96e25fdc0c257534159e77b0a1975039 Mon Sep 17 00:00:00 2001
From: Drew Tate
Date: Wed, 10 Apr 2024 20:06:08 +0200
Subject: [PATCH 30/55] [ES|QL] add validation for `VALUES` and `MV_SORT`
(#179874)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Adds validation support for
[VALUES](https://www.elastic.co/guide/en/elasticsearch/reference/master/esql-functions-operators.html#esql-agg-values)
and
[MV_SORT](https://www.elastic.co/guide/en/elasticsearch/reference/master/esql-functions-operators.html#esql-mv_sort).
Validation
Autocomplete
### 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
---------
Co-authored-by: Stratoula Kalafateli
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
packages/kbn-esql-ast/src/ast_helpers.ts | 19 ++-
packages/kbn-esql-ast/src/types.ts | 34 ++++-
.../src/autocomplete/autocomplete.test.ts | 57 ++++---
.../src/autocomplete/autocomplete.ts | 10 ++
.../src/autocomplete/factories.ts | 10 ++
.../src/definitions/aggs.ts | 18 +++
.../src/definitions/functions.ts | 32 +++-
.../src/definitions/types.ts | 17 +++
.../src/shared/helpers.ts | 43 ++++--
.../src/validation/errors.ts | 16 ++
.../esql_validation_meta_tests.json | 141 +++++++++++++-----
.../src/validation/types.ts | 4 +
.../src/validation/validation.test.ts | 97 ++++++++----
.../src/validation/validation.ts | 22 ++-
14 files changed, 402 insertions(+), 118 deletions(-)
diff --git a/packages/kbn-esql-ast/src/ast_helpers.ts b/packages/kbn-esql-ast/src/ast_helpers.ts
index 0f85e30b96c45..0b3c3eff25ada 100644
--- a/packages/kbn-esql-ast/src/ast_helpers.ts
+++ b/packages/kbn-esql-ast/src/ast_helpers.ts
@@ -113,14 +113,25 @@ export function createLiteral(
return;
}
const text = node.getText();
- return {
+
+ const partialLiteral: Omit = {
type: 'literal',
- literalType: type,
text,
name: text,
- value: type === 'number' ? Number(text) : text,
location: getPosition(node.symbol),
- incomplete: isMissingText(node.getText()),
+ incomplete: isMissingText(text),
+ };
+ if (type === 'number') {
+ return {
+ ...partialLiteral,
+ literalType: type,
+ value: Number(text),
+ };
+ }
+ return {
+ ...partialLiteral,
+ literalType: type,
+ value: text,
};
}
diff --git a/packages/kbn-esql-ast/src/types.ts b/packages/kbn-esql-ast/src/types.ts
index 4bce46d776671..f167276ae84a5 100644
--- a/packages/kbn-esql-ast/src/types.ts
+++ b/packages/kbn-esql-ast/src/types.ts
@@ -72,10 +72,38 @@ export interface ESQLList extends ESQLAstBaseItem {
values: ESQLLiteral[];
}
-export interface ESQLLiteral extends ESQLAstBaseItem {
+export type ESQLLiteral =
+ | ESQLNumberLiteral
+ | ESQLBooleanLiteral
+ | ESQLNullLiteral
+ | ESQLStringLiteral;
+
+// @internal
+export interface ESQLNumberLiteral extends ESQLAstBaseItem {
type: 'literal';
- literalType: 'string' | 'number' | 'boolean' | 'null';
- value: string | number;
+ literalType: 'number';
+ value: number;
+}
+
+// @internal
+export interface ESQLBooleanLiteral extends ESQLAstBaseItem {
+ type: 'literal';
+ literalType: 'boolean';
+ value: string;
+}
+
+// @internal
+export interface ESQLNullLiteral extends ESQLAstBaseItem {
+ type: 'literal';
+ literalType: 'null';
+ value: string;
+}
+
+// @internal
+export interface ESQLStringLiteral extends ESQLAstBaseItem {
+ type: 'literal';
+ literalType: 'string';
+ value: string;
}
export interface ESQLMessage {
diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts
index 6afe146560057..c51173cae68ce 100644
--- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts
+++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts
@@ -245,7 +245,7 @@ describe('autocomplete', () => {
// simulate the editor behaviour for sorting suggestions
.sort((a, b) => (a.sortText || '').localeCompare(b.sortText || ''));
for (const [index, receivedSuggestion] of suggestionInertTextSorted.entries()) {
- if (typeof expected[index] === 'string') {
+ if (typeof expected[index] !== 'object') {
expect(receivedSuggestion.text).toEqual(expected[index]);
} else {
// check all properties that are defined in the expected suggestion
@@ -1054,38 +1054,47 @@ describe('autocomplete', () => {
if (fn.name !== 'auto_bucket') {
for (const signature of fn.signatures) {
signature.params.forEach((param, i) => {
- if (i < signature.params.length - 1) {
+ if (i < signature.params.length) {
const canHaveMoreArgs =
+ i + 1 < (signature.minParams ?? 0) ||
signature.params.filter(({ optional }, j) => !optional && j > i).length > i;
testSuggestions(
`from a | eval ${fn.name}(${Array(i).fill('field').join(', ')}${i ? ',' : ''} )`,
- [
- ...getFieldNamesByType(param.type).map((f) => (canHaveMoreArgs ? `${f},` : f)),
- ...getFunctionSignaturesByReturnType(
- 'eval',
- param.type,
- { evalMath: true },
- undefined,
- [fn.name]
- ).map((l) => (canHaveMoreArgs ? `${l},` : l)),
- ...getLiteralsByType(param.type).map((d) => (canHaveMoreArgs ? `${d},` : d)),
- ]
+ param.literalOptions?.length
+ ? param.literalOptions.map((option) => `"${option}"${canHaveMoreArgs ? ',' : ''}`)
+ : [
+ ...getFieldNamesByType(param.type).map((f) =>
+ canHaveMoreArgs ? `${f},` : f
+ ),
+ ...getFunctionSignaturesByReturnType(
+ 'eval',
+ param.type,
+ { evalMath: true },
+ undefined,
+ [fn.name]
+ ).map((l) => (canHaveMoreArgs ? `${l},` : l)),
+ ...getLiteralsByType(param.type).map((d) => (canHaveMoreArgs ? `${d},` : d)),
+ ]
);
testSuggestions(
`from a | eval var0 = ${fn.name}(${Array(i).fill('field').join(', ')}${
i ? ',' : ''
} )`,
- [
- ...getFieldNamesByType(param.type).map((f) => (canHaveMoreArgs ? `${f},` : f)),
- ...getFunctionSignaturesByReturnType(
- 'eval',
- param.type,
- { evalMath: true },
- undefined,
- [fn.name]
- ).map((l) => (canHaveMoreArgs ? `${l},` : l)),
- ...getLiteralsByType(param.type).map((d) => (canHaveMoreArgs ? `${d},` : d)),
- ]
+ param.literalOptions?.length
+ ? param.literalOptions.map((option) => `"${option}"${canHaveMoreArgs ? ',' : ''}`)
+ : [
+ ...getFieldNamesByType(param.type).map((f) =>
+ canHaveMoreArgs ? `${f},` : f
+ ),
+ ...getFunctionSignaturesByReturnType(
+ 'eval',
+ param.type,
+ { evalMath: true },
+ undefined,
+ [fn.name]
+ ).map((l) => (canHaveMoreArgs ? `${l},` : l)),
+ ...getLiteralsByType(param.type).map((d) => (canHaveMoreArgs ? `${d},` : d)),
+ ]
);
}
});
diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts
index 84c5f59f49c31..ce9b2d2e0acf2 100644
--- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts
+++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts
@@ -68,6 +68,7 @@ import {
buildVariablesDefinitions,
buildOptionDefinition,
buildSettingDefinitions,
+ buildValueDefinitions,
} from './factories';
import { EDITOR_MARKER, SINGLE_BACKTICK } from '../shared/constants';
import { getAstContext, removeMarkerArgFromArgsList } from '../shared/context';
@@ -1082,6 +1083,15 @@ async function getFunctionArgsSuggestions(
return [];
});
+ const literalOptions = fnDefinition.signatures.reduce((acc, signature) => {
+ const literalOptionsForThisParameter = signature.params[argIndex]?.literalOptions;
+ return literalOptionsForThisParameter ? acc.concat(literalOptionsForThisParameter) : acc;
+ }, [] as string[]);
+
+ if (literalOptions.length) {
+ return buildValueDefinitions(literalOptions);
+ }
+
const arg = node.args[argIndex];
// the first signature is used as reference
diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts
index 3e243618ed7fa..2818634c58188 100644
--- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts
+++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts
@@ -165,6 +165,16 @@ export const buildConstantsDefinitions = (
sortText: 'A',
}));
+export const buildValueDefinitions = (values: string[]): SuggestionRawDefinition[] =>
+ values.map((value) => ({
+ label: `"${value}"`,
+ text: `"${value}"`,
+ detail: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.valueDefinition', {
+ defaultMessage: 'Literal value',
+ }),
+ kind: 'Value',
+ }));
+
export const buildNewVarDefinition = (label: string): SuggestionRawDefinition => {
return {
label,
diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/aggs.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/aggs.ts
index 94ab3106036fd..169ae23052ebc 100644
--- a/packages/kbn-esql-validation-autocomplete/src/definitions/aggs.ts
+++ b/packages/kbn-esql-validation-autocomplete/src/definitions/aggs.ts
@@ -175,4 +175,22 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
},
],
},
+ {
+ name: 'values',
+ type: 'agg',
+ description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.values', {
+ defaultMessage: 'Returns all values in a group as an array.',
+ }),
+ supportedCommands: ['stats'],
+ signatures: [
+ {
+ params: [{ name: 'expression', type: 'any', noNestingFunctions: true }],
+ returnType: 'any',
+ examples: [
+ 'from index | stats all_agents=values(agents.keyword)',
+ 'from index | stats all_sorted_agents=mv_sort(values(agents.keyword))',
+ ],
+ },
+ ],
+ },
]);
diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts
index 99f373879dbb6..f5badd2693234 100644
--- a/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts
+++ b/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts
@@ -97,7 +97,6 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [
],
validate: validateLogFunctions,
},
-
{
name: 'log',
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.logDoc', {
@@ -481,14 +480,9 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [
signatures: [
{
params: [{ name: 'field', type: 'string' }],
- returnType: 'version',
+ returnType: 'string',
examples: [`from index | EVAL version = to_version(stringField)`],
},
- {
- params: [{ name: 'field', type: 'version' }],
- returnType: 'version',
- examples: [`from index | EVAL version = to_version(versionField)`],
- },
],
},
{
@@ -924,6 +918,30 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [
},
],
},
+ {
+ name: 'mv_sort',
+ description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.mvSortDoc', {
+ defaultMessage: 'Sorts a multivalue expression in lexicographical order.',
+ }),
+ signatures: [
+ {
+ params: [
+ { name: 'field', type: 'any' },
+ {
+ name: 'order',
+ type: 'string',
+ optional: true,
+ literalOptions: ['asc', 'desc'],
+ },
+ ],
+ returnType: 'any',
+ examples: [
+ 'row a = [4, 2, -3, 2] | eval sorted = mv_sort(a)',
+ 'row a = ["b", "c", "a"] | sorted = mv_sort(a, "DESC")',
+ ],
+ },
+ ],
+ },
{
name: 'mv_avg',
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.mvAvgDoc', {
diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts
index 111846f1f515f..6d3488aacc003 100644
--- a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts
+++ b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts
@@ -23,7 +23,22 @@ export interface FunctionDefinition {
optional?: boolean;
noNestingFunctions?: boolean;
supportsWildcard?: boolean;
+ /**
+ * if set this indicates that the value must be a literal
+ * but can be any literal of the correct type
+ */
literalOnly?: boolean;
+ /**
+ * if provided this means that the value must be one
+ * of the options in the array iff the value is a literal.
+ *
+ * String values are case insensitive.
+ *
+ * If the value is not a literal, this field is ignored because
+ * we can't check the return value of a function to see if it
+ * matches one of the options prior to runtime.
+ */
+ literalOptions?: string[];
}>;
minParams?: number;
returnType: string;
@@ -87,3 +102,5 @@ export type SignatureType =
| FunctionDefinition['signatures'][number]
| CommandOptionsDefinition['signature'];
export type SignatureArgType = SignatureType['params'][number];
+
+export type FunctionArgSignature = FunctionDefinition['signatures'][number]['params'][number];
diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts
index 8eab378214a47..1d439aa3a0b9e 100644
--- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts
+++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts
@@ -34,6 +34,7 @@ import {
import type {
CommandDefinition,
CommandOptionsDefinition,
+ FunctionArgSignature,
FunctionDefinition,
SignatureArgType,
} from '../definitions/types';
@@ -179,6 +180,8 @@ export function getFunctionDefinition(name: string) {
return buildFunctionLookup().get(name.toLowerCase());
}
+const unwrapStringLiteralQuotes = (value: string) => value.slice(1, -1);
+
function buildCommandLookup() {
if (!commandLookups) {
commandLookups = commandDefinitions.reduce((memo, def) => {
@@ -337,8 +340,28 @@ export function inKnownTimeInterval(item: ESQLTimeInterval): boolean {
return timeLiterals.some(({ name }) => name === item.unit.toLowerCase());
}
+/**
+ * Checks if this argument is one of the possible options
+ * if they are defined on the arg definition.
+ *
+ * TODO - Consider merging with isEqualType to create a unified arg validation function
+ */
+export function isValidLiteralOption(arg: ESQLLiteral, argDef: FunctionArgSignature) {
+ return (
+ arg.literalType === 'string' &&
+ argDef.literalOptions &&
+ !argDef.literalOptions
+ .map((option) => option.toLowerCase())
+ .includes(unwrapStringLiteralQuotes(arg.value).toLowerCase())
+ );
+}
+
+/**
+ * Checks if an AST argument is of the correct type
+ * given the definition.
+ */
export function isEqualType(
- item: ESQLSingleAstItem,
+ arg: ESQLSingleAstItem,
argDef: SignatureArgType,
references: ReferenceMaps,
parentCommand?: string,
@@ -348,24 +371,24 @@ export function isEqualType(
if (argType === 'any') {
return true;
}
- if (item.type === 'literal') {
- return compareLiteralType(argType, item);
+ if (arg.type === 'literal') {
+ return compareLiteralType(argType, arg);
}
- if (item.type === 'function') {
- if (isSupportedFunction(item.name, parentCommand).supported) {
- const fnDef = buildFunctionLookup().get(item.name)!;
+ if (arg.type === 'function') {
+ if (isSupportedFunction(arg.name, parentCommand).supported) {
+ const fnDef = buildFunctionLookup().get(arg.name)!;
return fnDef.signatures.some((signature) => argType === signature.returnType);
}
}
- if (item.type === 'timeInterval') {
- return argType === 'time_literal' && inKnownTimeInterval(item);
+ if (arg.type === 'timeInterval') {
+ return argType === 'time_literal' && inKnownTimeInterval(arg);
}
- if (item.type === 'column') {
+ if (arg.type === 'column') {
if (argType === 'column') {
// anything goes, so avoid any effort here
return true;
}
- const hit = getColumnHit(nameHit ?? item.name, references);
+ const hit = getColumnHit(nameHit ?? arg.name, references);
const validHit = hit;
if (!validHit) {
return false;
diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts b/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts
index b7e02f10683f3..5c8608e37ea7c 100644
--- a/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts
+++ b/packages/kbn-esql-validation-autocomplete/src/validation/errors.ts
@@ -300,6 +300,22 @@ function getMessageAndTypeFromId({
),
type: 'error',
};
+ case 'unsupportedLiteralOption':
+ return {
+ message: i18n.translate(
+ 'kbn-esql-validation-autocomplete.esql.validation.unsupportedLiteralOption',
+ {
+ defaultMessage:
+ 'Invalid option [{value}] for {name}. Supported options: [{supportedOptions}].',
+ values: {
+ name: out.name,
+ value: out.value,
+ supportedOptions: out.supportedOptions,
+ },
+ }
+ ),
+ type: 'warning',
+ };
case 'expectedConstant':
return {
message: i18n.translate(
diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json
index c64d7235b907a..4adae6dc511eb 100644
--- a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json
+++ b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json
@@ -1370,6 +1370,16 @@
"error": [],
"warning": []
},
+ {
+ "query": "row var = mv_sort(\"a\", \"asc\")",
+ "error": [],
+ "warning": []
+ },
+ {
+ "query": "row mv_sort(\"a\", \"asc\")",
+ "error": [],
+ "warning": []
+ },
{
"query": "row var = mv_sum(5)",
"error": [],
@@ -2005,21 +2015,6 @@
"error": [],
"warning": []
},
- {
- "query": "row var = to_version(\"a\")",
- "error": [],
- "warning": []
- },
- {
- "query": "row to_version(\"a\")",
- "error": [],
- "warning": []
- },
- {
- "query": "row var = to_ver(\"a\")",
- "error": [],
- "warning": []
- },
{
"query": "row var = trim(\"a\")",
"error": [],
@@ -2333,6 +2328,23 @@
],
"warning": []
},
+ {
+ "query": "row var = mv_sort([\"a\", \"b\"], \"bogus\")",
+ "error": [],
+ "warning": [
+ "Invalid option [\"bogus\"] for mv_sort. Supported options: [\"asc\", \"desc\"]."
+ ]
+ },
+ {
+ "query": "row var = mv_sort([\"a\", \"b\"], \"ASC\")",
+ "error": [],
+ "warning": []
+ },
+ {
+ "query": "row var = mv_sort([\"a\", \"b\"], \"DESC\")",
+ "error": [],
+ "warning": []
+ },
{
"query": "row 1 anno",
"error": [
@@ -5751,6 +5763,18 @@
],
"warning": []
},
+ {
+ "query": "from a_index | where length(to_version(stringField)) > 0",
+ "error": [],
+ "warning": []
+ },
+ {
+ "query": "from a_index | where length(to_version(numberField)) > 0",
+ "error": [
+ "Argument of [to_version] must be [string], found value [numberField] type [number]"
+ ],
+ "warning": []
+ },
{
"query": "from a_index | where length(trim(stringField)) > 0",
"error": [],
@@ -6883,6 +6907,34 @@
],
"warning": []
},
+ {
+ "query": "from a_index | eval var = values(stringField)",
+ "error": [
+ "EVAL does not support function values"
+ ],
+ "warning": []
+ },
+ {
+ "query": "from a_index | eval var = values(stringField) > 0",
+ "error": [
+ "EVAL does not support function values"
+ ],
+ "warning": []
+ },
+ {
+ "query": "from a_index | eval values(stringField)",
+ "error": [
+ "EVAL does not support function values"
+ ],
+ "warning": []
+ },
+ {
+ "query": "from a_index | eval values(stringField) > 0",
+ "error": [
+ "EVAL does not support function values"
+ ],
+ "warning": []
+ },
{
"query": "from a_index | eval var = abs(numberField)",
"error": [],
@@ -7871,6 +7923,16 @@
],
"warning": []
},
+ {
+ "query": "from a_index | eval var = mv_sort(stringField, \"asc\")",
+ "error": [],
+ "warning": []
+ },
+ {
+ "query": "from a_index | eval mv_sort(stringField, \"asc\")",
+ "error": [],
+ "warning": []
+ },
{
"query": "from a_index | eval var = mv_sum(numberField)",
"error": [],
@@ -8821,28 +8883,6 @@
],
"warning": []
},
- {
- "query": "from a_index | eval var = to_version(stringField)",
- "error": [],
- "warning": []
- },
- {
- "query": "from a_index | eval to_version(stringField)",
- "error": [],
- "warning": []
- },
- {
- "query": "from a_index | eval var = to_ver(stringField)",
- "error": [],
- "warning": []
- },
- {
- "query": "from a_index | eval var = to_version(*)",
- "error": [
- "Using wildcards (*) in to_version is not allowed"
- ],
- "warning": []
- },
{
"query": "from a_index | eval var = trim(stringField)",
"error": [],
@@ -9413,6 +9453,23 @@
],
"warning": []
},
+ {
+ "query": "from a_index | eval mv_sort([\"a\", \"b\"], \"bogus\")",
+ "error": [],
+ "warning": [
+ "Invalid option [\"bogus\"] for mv_sort. Supported options: [\"asc\", \"desc\"]."
+ ]
+ },
+ {
+ "query": "from a_index | eval mv_sort([\"a\", \"b\"], \"ASC\")",
+ "error": [],
+ "warning": []
+ },
+ {
+ "query": "from a_index | eval mv_sort([\"a\", \"b\"], \"DESC\")",
+ "error": [],
+ "warning": []
+ },
{
"query": "from a_index | eval 1 anno",
"error": [
@@ -12175,6 +12232,16 @@
],
"warning": []
},
+ {
+ "query": "from a_index | stats var = values(stringField)",
+ "error": [],
+ "warning": []
+ },
+ {
+ "query": "from a_index | stats values(stringField)",
+ "error": [],
+ "warning": []
+ },
{
"query": "FROM index\n | EVAL numberField * 3.281\n | STATS avg_numberField = AVG(`numberField * 3.281`)",
"error": [],
diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/types.ts b/packages/kbn-esql-validation-autocomplete/src/validation/types.ts
index 0aec2b79eaa89..aaf98773eca2c 100644
--- a/packages/kbn-esql-validation-autocomplete/src/validation/types.ts
+++ b/packages/kbn-esql-validation-autocomplete/src/validation/types.ts
@@ -96,6 +96,10 @@ export interface ValidationErrors {
message: string;
type: { name: string; command: string; option: string };
};
+ unsupportedLiteralOption: {
+ message: string;
+ type: { name: string; value: string; supportedOptions: string };
+ };
shadowFieldType: {
message: string;
type: { field: string; fieldType: string; newType: string };
diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts
index 7d19f4c23cad1..0e48e60b9cc3d 100644
--- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts
+++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts
@@ -164,9 +164,17 @@ function getFieldMapping(
string: `"a"`,
number: '5',
};
- return params.map(({ name: _name, type, literalOnly, ...rest }) => {
+ return params.map(({ name: _name, type, literalOnly, literalOptions, ...rest }) => {
const typeString: string = type;
if (fieldTypes.includes(typeString)) {
+ if (useLiterals && literalOptions) {
+ return {
+ name: `"${literalOptions[0]}"`,
+ type,
+ ...rest,
+ };
+ }
+
const fieldName =
literalOnly && typeString in literalValues
? literalValues[typeString as keyof typeof literalValues]!
@@ -187,7 +195,7 @@ function getFieldMapping(
...rest,
};
}
- if (/[]$/.test(typeString)) {
+ if (/\[\]$/.test(typeString)) {
return {
name: getMultiValue(typeString),
type,
@@ -198,7 +206,7 @@ function getFieldMapping(
});
}
-function generateWrongMappingForArgs(
+function generateIncorrectlyTypedParameters(
name: string,
signatures: FunctionDefinition['signatures'],
currentParams: FunctionDefinition['signatures'][number]['params'],
@@ -208,28 +216,30 @@ function generateWrongMappingForArgs(
string: `"a"`,
number: '5',
};
- const wrongFieldMapping = currentParams.map(({ name: _name, literalOnly, type, ...rest }, i) => {
- // this thing is complex enough, let's not make it harder for constants
- if (literalOnly) {
- return { name: literalValues[type as keyof typeof literalValues], type, ...rest };
+ const wrongFieldMapping = currentParams.map(
+ ({ name: _name, literalOnly, literalOptions, type, ...rest }, i) => {
+ // this thing is complex enough, let's not make it harder for constants
+ if (literalOnly) {
+ return { name: literalValues[type as keyof typeof literalValues], type, ...rest };
+ }
+ const canBeFieldButNotString = Boolean(
+ fieldTypes.filter((t) => t !== 'string').includes(type) &&
+ signatures.every(({ params: fnParams }) => fnParams[i].type !== 'string')
+ );
+ const canBeFieldButNotNumber =
+ fieldTypes.filter((t) => t !== 'number').includes(type) &&
+ signatures.every(({ params: fnParams }) => fnParams[i].type !== 'number');
+ const isLiteralType = /literal$/.test(type);
+ // pick a field name purposely wrong
+ const nameValue =
+ canBeFieldButNotString || isLiteralType
+ ? values.stringField
+ : canBeFieldButNotNumber
+ ? values.numberField
+ : values.booleanField;
+ return { name: nameValue, type, ...rest };
}
- const canBeFieldButNotString = Boolean(
- fieldTypes.filter((t) => t !== 'string').includes(type) &&
- signatures.every(({ params: fnParams }) => fnParams[i].type !== 'string')
- );
- const canBeFieldButNotNumber =
- fieldTypes.filter((t) => t !== 'number').includes(type) &&
- signatures.every(({ params: fnParams }) => fnParams[i].type !== 'number');
- const isLiteralType = /literal$/.test(type);
- // pick a field name purposely wrong
- const nameValue =
- canBeFieldButNotString || isLiteralType
- ? values.stringField
- : canBeFieldButNotNumber
- ? values.numberField
- : values.booleanField;
- return { name: nameValue, type, ...rest };
- });
+ );
const generatedFieldTypes = {
[values.stringField]: 'string',
@@ -250,6 +260,7 @@ function generateWrongMappingForArgs(
return `Argument of [${name}] must be [${type}], found value [${fieldName}] type [${generatedFieldTypes[fieldName]}]`;
})
.filter(nonNullable);
+
return { wrongFieldMapping, expectedErrors };
}
@@ -565,7 +576,7 @@ describe('validation logic', () => {
// the right error message
if (
params.every(({ type }) => type !== 'any') &&
- !['auto_bucket', 'to_version'].includes(name)
+ !['auto_bucket', 'to_version', 'mv_sort'].includes(name)
) {
// now test nested functions
const fieldMappingWithNestedFunctions = getFieldMapping(params, {
@@ -585,11 +596,15 @@ describe('validation logic', () => {
testErrorsAndWarnings(`row var = ${signatureString}`, []);
- const { wrongFieldMapping, expectedErrors } = generateWrongMappingForArgs(
+ const { wrongFieldMapping, expectedErrors } = generateIncorrectlyTypedParameters(
name,
signatures,
params,
- { stringField: '"a"', numberField: '5', booleanField: 'true' }
+ {
+ stringField: '"a"',
+ numberField: '5',
+ booleanField: 'true',
+ }
);
const wrongSignatureString = tweakSignatureForRowCommand(
getFunctionSignatures(
@@ -634,6 +649,15 @@ describe('validation logic', () => {
]);
}
+ testErrorsAndWarnings(
+ `row var = mv_sort(["a", "b"], "bogus")`,
+ [],
+ ['Invalid option ["bogus"] for mv_sort. Supported options: ["asc", "desc"].']
+ );
+
+ testErrorsAndWarnings(`row var = mv_sort(["a", "b"], "ASC")`, []);
+ testErrorsAndWarnings(`row var = mv_sort(["a", "b"], "DESC")`, []);
+
describe('date math', () => {
testErrorsAndWarnings('row 1 anno', [
'ROW does not support [date_period] in expression [1 anno]',
@@ -1187,7 +1211,7 @@ describe('validation logic', () => {
[]
);
- const { wrongFieldMapping, expectedErrors } = generateWrongMappingForArgs(
+ const { wrongFieldMapping, expectedErrors } = generateIncorrectlyTypedParameters(
name,
signatures,
params,
@@ -1435,7 +1459,7 @@ describe('validation logic', () => {
// the right error message
if (
params.every(({ type }) => type !== 'any') &&
- !['auto_bucket', 'to_version'].includes(name)
+ !['auto_bucket', 'to_version', 'mv_sort'].includes(name)
) {
// now test nested functions
const fieldMappingWithNestedFunctions = getFieldMapping(params, {
@@ -1455,7 +1479,7 @@ describe('validation logic', () => {
}`
);
- const { wrongFieldMapping, expectedErrors } = generateWrongMappingForArgs(
+ const { wrongFieldMapping, expectedErrors } = generateIncorrectlyTypedParameters(
name,
signatures,
params,
@@ -1676,6 +1700,15 @@ describe('validation logic', () => {
"SyntaxError: mismatched input '' expecting {',', ')'}",
]);
+ testErrorsAndWarnings(
+ 'from a_index | eval mv_sort(["a", "b"], "bogus")',
+ [],
+ ['Invalid option ["bogus"] for mv_sort. Supported options: ["asc", "desc"].']
+ );
+
+ testErrorsAndWarnings(`from a_index | eval mv_sort(["a", "b"], "ASC")`, []);
+ testErrorsAndWarnings(`from a_index | eval mv_sort(["a", "b"], "DESC")`, []);
+
describe('date math', () => {
testErrorsAndWarnings('from a_index | eval 1 anno', [
'EVAL does not support [date_period] in expression [1 anno]',
@@ -2064,7 +2097,7 @@ describe('validation logic', () => {
// the right error message
if (
params.every(({ type }) => type !== 'any') &&
- !['auto_bucket', 'to_version'].includes(name)
+ !['auto_bucket', 'to_version', 'mv_sort'].includes(name)
) {
// now test nested functions
const fieldMappingWithNestedAggsFunctions = getFieldMapping(params, {
@@ -2103,7 +2136,7 @@ describe('validation logic', () => {
}`,
nestedAggsExpectedErrors
);
- const { wrongFieldMapping, expectedErrors } = generateWrongMappingForArgs(
+ const { wrongFieldMapping, expectedErrors } = generateIncorrectlyTypedParameters(
name,
signatures,
params,
diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts
index c8e60bf4c7e96..3d508dc93389f 100644
--- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts
+++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts
@@ -22,6 +22,7 @@ import type {
import {
CommandModeDefinition,
CommandOptionsDefinition,
+ FunctionArgSignature,
FunctionDefinition,
SignatureArgType,
} from '../definitions/types';
@@ -51,6 +52,7 @@ import {
isSettingItem,
isAssignment,
isVariable,
+ isValidLiteralOption,
} from '../shared/helpers';
import { collectVariables } from '../shared/variables';
import { getMessageFromId, getUnknownTypeLabel } from './errors';
@@ -75,12 +77,30 @@ import {
function validateFunctionLiteralArg(
astFunction: ESQLFunction,
actualArg: ESQLAstItem,
- argDef: SignatureArgType,
+ argDef: FunctionArgSignature,
references: ReferenceMaps,
parentCommand: string
) {
const messages: ESQLMessage[] = [];
if (isLiteralItem(actualArg)) {
+ if (
+ actualArg.literalType === 'string' &&
+ argDef.literalOptions &&
+ isValidLiteralOption(actualArg, argDef)
+ ) {
+ messages.push(
+ getMessageFromId({
+ messageId: 'unsupportedLiteralOption',
+ values: {
+ name: astFunction.name,
+ value: actualArg.value,
+ supportedOptions: argDef.literalOptions?.map((option) => `"${option}"`).join(', '),
+ },
+ locations: actualArg.location,
+ })
+ );
+ }
+
if (!isEqualType(actualArg, argDef, references, parentCommand)) {
messages.push(
getMessageFromId({
From 03236b7f7907d2ed05627929afee33c0c3d0f3e5 Mon Sep 17 00:00:00 2001
From: Tre
Date: Wed, 10 Apr 2024 19:07:13 +0100
Subject: [PATCH 31/55] [Serverless] Skip "Trained models list" suite on MKI
(#180488)
## Summary
Skip "Trained models list" suite on MKI
Details about the failure in
https://github.com/elastic/kibana/issues/180481
---
.../test_suites/security/ml/trained_models_list.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
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
index 92bdbd6ffab65..1d6cc3c0e5e25 100644
--- 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
@@ -11,7 +11,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const svlMl = getService('svlMl');
const PageObjects = getPageObjects(['svlCommonPage']);
- describe('Trained models list', () => {
+ // failsOnMKI, see https://github.com/elastic/kibana/issues/180481
+ describe('Trained models list', function () {
+ this.tags(['failsOnMKI']);
+
before(async () => {
await PageObjects.svlCommonPage.login();
await ml.api.syncSavedObjects();
From 4db3fedd8c700c98c0cc6049a5012bc2fcc12138 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?=
Date: Wed, 10 Apr 2024 20:11:22 +0200
Subject: [PATCH 32/55] [Core Usage Stats] Increment in batches (#180405)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../src/server.test.mocks.ts | 6 +
.../core-root-server-internal/tsconfig.json | 1 +
.../src/core_usage_data_service.test.ts | 4 +
.../src/core_usage_data_service.ts | 7 +-
.../src/core_usage_stats_client.test.ts | 641 +++++++++++++-----
.../src/core_usage_stats_client.ts | 90 ++-
6 files changed, 567 insertions(+), 182 deletions(-)
diff --git a/packages/core/root/core-root-server-internal/src/server.test.mocks.ts b/packages/core/root/core-root-server-internal/src/server.test.mocks.ts
index d1fd4d8ba1266..6e4f18637ab15 100644
--- a/packages/core/root/core-root-server-internal/src/server.test.mocks.ts
+++ b/packages/core/root/core-root-server-internal/src/server.test.mocks.ts
@@ -68,6 +68,7 @@ jest.doMock('@kbn/core-ui-settings-server-internal', () => ({
}));
import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mocks';
+import { coreUsageDataServiceMock } from '@kbn/core-usage-data-server-mocks';
export const mockCustomBrandingService = customBrandingServiceMock.create();
jest.doMock('@kbn/core-custom-branding-server-internal', () => ({
@@ -138,3 +139,8 @@ export const mockSecurityService = securityServiceMock.create();
jest.doMock('@kbn/core-security-server-internal', () => ({
SecurityService: jest.fn(() => mockSecurityService),
}));
+
+export const mockUsageDataService = coreUsageDataServiceMock.create();
+jest.doMock('@kbn/core-usage-data-server-internal', () => ({
+ CoreUsageDataService: jest.fn(() => mockUsageDataService),
+}));
diff --git a/packages/core/root/core-root-server-internal/tsconfig.json b/packages/core/root/core-root-server-internal/tsconfig.json
index 8c8340b2aca0c..6eed4c2e59413 100644
--- a/packages/core/root/core-root-server-internal/tsconfig.json
+++ b/packages/core/root/core-root-server-internal/tsconfig.json
@@ -73,6 +73,7 @@
"@kbn/core-user-settings-server-mocks",
"@kbn/core-security-server-mocks",
"@kbn/core-security-server-internal",
+ "@kbn/core-usage-data-server-mocks",
],
"exclude": [
"target/**/*",
diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts
index f03e26d70cb11..11602704cf0f0 100644
--- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts
+++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts
@@ -86,6 +86,10 @@ describe('CoreUsageDataService', () => {
service = new CoreUsageDataService(coreContext);
});
+ afterEach(() => {
+ service.stop();
+ });
+
describe('setup', () => {
it('creates internal repository', async () => {
const http = httpServiceMock.createInternalSetupContract();
diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts
index 0d2ae2ad0f2f7..4b4ab735d5eea 100644
--- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts
+++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts
@@ -496,7 +496,12 @@ export class CoreUsageDataService
const getClient = () => {
const debugLogger = (message: string) => this.logger.debug(message);
- return new CoreUsageStatsClient(debugLogger, http.basePath, internalRepositoryPromise);
+ return new CoreUsageStatsClient(
+ debugLogger,
+ http.basePath,
+ internalRepositoryPromise,
+ this.stop$
+ );
};
this.coreUsageStatsClient = getClient();
diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts
index 86d1e1bbd8712..6c30d6ce2c8ff 100644
--- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts
+++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import { Subject } from 'rxjs';
import { httpServerMock, httpServiceMock } from '@kbn/core-http-server-mocks';
import { savedObjectsRepositoryMock } from '@kbn/core-saved-objects-api-server-mocks';
import {
@@ -38,6 +39,7 @@ import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
import { CoreUsageStatsClient } from '.';
describe('CoreUsageStatsClient', () => {
+ const stop$ = new Subject();
const setup = (namespace?: string) => {
const debugLoggerMock = jest.fn();
const basePathMock = httpServiceMock.createBasePath();
@@ -47,7 +49,8 @@ describe('CoreUsageStatsClient', () => {
const usageStatsClient = new CoreUsageStatsClient(
debugLoggerMock,
basePathMock,
- Promise.resolve(repositoryMock)
+ Promise.resolve(repositoryMock),
+ stop$
);
return { usageStatsClient, debugLoggerMock, basePathMock, repositoryMock };
};
@@ -58,6 +61,115 @@ describe('CoreUsageStatsClient', () => {
}; // as long as these header fields are truthy, this will be treated like a first-party request
const incrementOptions = { refresh: false };
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ stop$.next();
+ });
+
+ describe('Request-batching', () => {
+ it.each([
+ { triggerName: 'timer-based', triggerFn: async () => await jest.runOnlyPendingTimersAsync() },
+ {
+ triggerName: 'forced-flush',
+ triggerFn: (usageStatsClient: CoreUsageStatsClient) => {
+ // eslint-disable-next-line dot-notation
+ usageStatsClient['flush$'].next();
+ },
+ },
+ ])('batches multiple increments into one ($triggerName)', async ({ triggerFn }) => {
+ const { usageStatsClient, repositoryMock } = setup();
+
+ // First request
+ const request = httpServerMock.createKibanaRequest();
+ await usageStatsClient.incrementSavedObjectsBulkCreate({
+ request,
+ } as BaseIncrementOptions);
+
+ // Second request
+ const kibanaRequest = httpServerMock.createKibanaRequest({
+ headers: firstPartyRequestHeaders,
+ });
+ await usageStatsClient.incrementSavedObjectsBulkCreate({
+ request: kibanaRequest,
+ } as BaseIncrementOptions);
+
+ // Run trigger
+ await triggerFn(usageStatsClient);
+
+ expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
+ expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
+ CORE_USAGE_STATS_TYPE,
+ CORE_USAGE_STATS_ID,
+ [
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 2 },
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 2 },
+ {
+ fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
+ {
+ fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
+ ],
+ incrementOptions
+ );
+ });
+
+ it('triggers when the queue is too large', async () => {
+ const { usageStatsClient, repositoryMock } = setup();
+
+ // Trigger enough requests to overflow the queue
+ const request = httpServerMock.createKibanaRequest();
+ await Promise.all(
+ [...new Array(10_001).keys()].map(() =>
+ usageStatsClient.incrementSavedObjectsBulkCreate({
+ request,
+ } as BaseIncrementOptions)
+ )
+ );
+
+ // It sends all elements in the max batch
+ expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
+ expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith(
+ 1,
+ CORE_USAGE_STATS_TYPE,
+ CORE_USAGE_STATS_ID,
+ [
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 10_000 },
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 10_000 },
+ {
+ fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 10_000,
+ },
+ ],
+ incrementOptions
+ );
+
+ // After timer, it sends the remainder event
+ await jest.runOnlyPendingTimersAsync();
+
+ expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(2);
+ expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith(
+ 2,
+ CORE_USAGE_STATS_TYPE,
+ CORE_USAGE_STATS_ID,
+ [
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
+ ],
+ incrementOptions
+ );
+ });
+ });
+
describe('#getUsageStats', () => {
it('returns empty object when encountering a repository error', async () => {
const { usageStatsClient, repositoryMock } = setup();
@@ -93,6 +205,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as BaseIncrementOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -103,14 +216,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkCreate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_CREATE_STATS_PREFIX}.total`,
- `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`,
- `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -123,14 +240,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkCreate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_CREATE_STATS_PREFIX}.total`,
- `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`,
- `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -143,14 +264,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkCreate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_CREATE_STATS_PREFIX}.total`,
- `${BULK_CREATE_STATS_PREFIX}.namespace.custom.total`,
- `${BULK_CREATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_CREATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -168,6 +293,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as BaseIncrementOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -178,14 +304,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkGet({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_GET_STATS_PREFIX}.total`,
- `${BULK_GET_STATS_PREFIX}.namespace.default.total`,
- `${BULK_GET_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ { fieldName: `${BULK_GET_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_GET_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_GET_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -198,14 +328,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkGet({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_GET_STATS_PREFIX}.total`,
- `${BULK_GET_STATS_PREFIX}.namespace.default.total`,
- `${BULK_GET_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${BULK_GET_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_GET_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_GET_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -218,14 +352,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkGet({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_GET_STATS_PREFIX}.total`,
- `${BULK_GET_STATS_PREFIX}.namespace.custom.total`,
- `${BULK_GET_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${BULK_GET_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_GET_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_GET_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -243,6 +381,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as BaseIncrementOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -253,14 +392,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkResolve({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_RESOLVE_STATS_PREFIX}.total`,
- `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.total`,
- `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -273,14 +416,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkResolve({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_RESOLVE_STATS_PREFIX}.total`,
- `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.total`,
- `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -293,14 +440,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkResolve({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_RESOLVE_STATS_PREFIX}.total`,
- `${BULK_RESOLVE_STATS_PREFIX}.namespace.custom.total`,
- `${BULK_RESOLVE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_RESOLVE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -318,6 +469,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as BaseIncrementOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -328,14 +480,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkUpdate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_UPDATE_STATS_PREFIX}.total`,
- `${BULK_UPDATE_STATS_PREFIX}.namespace.default.total`,
- `${BULK_UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ { fieldName: `${BULK_UPDATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -348,14 +504,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkUpdate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_UPDATE_STATS_PREFIX}.total`,
- `${BULK_UPDATE_STATS_PREFIX}.namespace.default.total`,
- `${BULK_UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${BULK_UPDATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -368,14 +528,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkUpdate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_UPDATE_STATS_PREFIX}.total`,
- `${BULK_UPDATE_STATS_PREFIX}.namespace.custom.total`,
- `${BULK_UPDATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${BULK_UPDATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_UPDATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -393,6 +557,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as BaseIncrementOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -403,14 +568,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsCreate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${CREATE_STATS_PREFIX}.total`,
- `${CREATE_STATS_PREFIX}.namespace.default.total`,
- `${CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ { fieldName: `${CREATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -423,14 +592,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsCreate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${CREATE_STATS_PREFIX}.total`,
- `${CREATE_STATS_PREFIX}.namespace.default.total`,
- `${CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${CREATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${CREATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -443,14 +616,15 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsCreate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${CREATE_STATS_PREFIX}.total`,
- `${CREATE_STATS_PREFIX}.namespace.custom.total`,
- `${CREATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${CREATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${CREATE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ { fieldName: `${CREATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 },
],
incrementOptions
);
@@ -468,6 +642,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as BaseIncrementOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -478,14 +653,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkDelete({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_DELETE_STATS_PREFIX}.total`,
- `${BULK_DELETE_STATS_PREFIX}.namespace.default.total`,
- `${BULK_DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ { fieldName: `${BULK_DELETE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -498,14 +677,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkDelete({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_DELETE_STATS_PREFIX}.total`,
- `${BULK_DELETE_STATS_PREFIX}.namespace.default.total`,
- `${BULK_DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${BULK_DELETE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -518,14 +701,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsBulkDelete({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${BULK_DELETE_STATS_PREFIX}.total`,
- `${BULK_DELETE_STATS_PREFIX}.namespace.custom.total`,
- `${BULK_DELETE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${BULK_DELETE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ {
+ fieldName: `${BULK_DELETE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -543,6 +730,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as BaseIncrementOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -553,14 +741,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsDelete({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${DELETE_STATS_PREFIX}.total`,
- `${DELETE_STATS_PREFIX}.namespace.default.total`,
- `${DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ { fieldName: `${DELETE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${DELETE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -573,14 +765,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsDelete({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${DELETE_STATS_PREFIX}.total`,
- `${DELETE_STATS_PREFIX}.namespace.default.total`,
- `${DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${DELETE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${DELETE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -593,14 +789,15 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsDelete({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${DELETE_STATS_PREFIX}.total`,
- `${DELETE_STATS_PREFIX}.namespace.custom.total`,
- `${DELETE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${DELETE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${DELETE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ { fieldName: `${DELETE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 },
],
incrementOptions
);
@@ -618,6 +815,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as BaseIncrementOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -628,14 +826,15 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsFind({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${FIND_STATS_PREFIX}.total`,
- `${FIND_STATS_PREFIX}.namespace.default.total`,
- `${FIND_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ { fieldName: `${FIND_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${FIND_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ { fieldName: `${FIND_STATS_PREFIX}.namespace.default.kibanaRequest.no`, incrementBy: 1 },
],
incrementOptions
);
@@ -648,14 +847,15 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsFind({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${FIND_STATS_PREFIX}.total`,
- `${FIND_STATS_PREFIX}.namespace.default.total`,
- `${FIND_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${FIND_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${FIND_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ { fieldName: `${FIND_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, incrementBy: 1 },
],
incrementOptions
);
@@ -668,14 +868,15 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsFind({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${FIND_STATS_PREFIX}.total`,
- `${FIND_STATS_PREFIX}.namespace.custom.total`,
- `${FIND_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${FIND_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${FIND_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ { fieldName: `${FIND_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 },
],
incrementOptions
);
@@ -693,6 +894,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as BaseIncrementOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -703,14 +905,15 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsGet({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${GET_STATS_PREFIX}.total`,
- `${GET_STATS_PREFIX}.namespace.default.total`,
- `${GET_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ { fieldName: `${GET_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${GET_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ { fieldName: `${GET_STATS_PREFIX}.namespace.default.kibanaRequest.no`, incrementBy: 1 },
],
incrementOptions
);
@@ -723,14 +926,15 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsGet({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${GET_STATS_PREFIX}.total`,
- `${GET_STATS_PREFIX}.namespace.default.total`,
- `${GET_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${GET_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${GET_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ { fieldName: `${GET_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, incrementBy: 1 },
],
incrementOptions
);
@@ -743,14 +947,15 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsGet({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${GET_STATS_PREFIX}.total`,
- `${GET_STATS_PREFIX}.namespace.custom.total`,
- `${GET_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${GET_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${GET_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ { fieldName: `${GET_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 },
],
incrementOptions
);
@@ -768,6 +973,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as BaseIncrementOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -778,14 +984,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsResolve({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${RESOLVE_STATS_PREFIX}.total`,
- `${RESOLVE_STATS_PREFIX}.namespace.default.total`,
- `${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ { fieldName: `${RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${RESOLVE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -798,14 +1008,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsResolve({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${RESOLVE_STATS_PREFIX}.total`,
- `${RESOLVE_STATS_PREFIX}.namespace.default.total`,
- `${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${RESOLVE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${RESOLVE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -818,14 +1032,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsResolve({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${RESOLVE_STATS_PREFIX}.total`,
- `${RESOLVE_STATS_PREFIX}.namespace.custom.total`,
- `${RESOLVE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${RESOLVE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${RESOLVE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ {
+ fieldName: `${RESOLVE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -843,6 +1061,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as BaseIncrementOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -853,14 +1072,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsUpdate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${UPDATE_STATS_PREFIX}.total`,
- `${UPDATE_STATS_PREFIX}.namespace.default.total`,
- `${UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ { fieldName: `${UPDATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${UPDATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -873,14 +1096,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsUpdate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${UPDATE_STATS_PREFIX}.total`,
- `${UPDATE_STATS_PREFIX}.namespace.default.total`,
- `${UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${UPDATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${UPDATE_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -893,14 +1120,15 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsUpdate({
request,
} as BaseIncrementOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${UPDATE_STATS_PREFIX}.total`,
- `${UPDATE_STATS_PREFIX}.namespace.custom.total`,
- `${UPDATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${UPDATE_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${UPDATE_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ { fieldName: `${UPDATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 },
],
incrementOptions
);
@@ -918,6 +1146,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as IncrementSavedObjectsImportOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
});
@@ -928,17 +1157,21 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsImport({
request,
} as IncrementSavedObjectsImportOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${IMPORT_STATS_PREFIX}.total`,
- `${IMPORT_STATS_PREFIX}.namespace.default.total`,
- `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
- `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
- `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`,
- `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`,
+ { fieldName: `${IMPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
+ { fieldName: `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`, incrementBy: 1 },
],
incrementOptions
);
@@ -954,22 +1187,27 @@ describe('CoreUsageStatsClient', () => {
overwrite: true,
compatibilityMode: true,
} as IncrementSavedObjectsImportOptions);
+ await jest.runOnlyPendingTimersAsync();
await usageStatsClient.incrementSavedObjectsImport({
request,
createNewCopies: false,
overwrite: true,
compatibilityMode: true,
} as IncrementSavedObjectsImportOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(2);
expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith(
1,
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${IMPORT_STATS_PREFIX}.total`,
- `${IMPORT_STATS_PREFIX}.namespace.default.total`,
- `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
- `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
+ { fieldName: `${IMPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
+ { fieldName: `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, incrementBy: 1 },
// excludes 'overwriteEnabled.yes', 'overwriteEnabled.no', 'compatibilityModeEnabled.yes`, and
// `compatibilityModeEnabled.no` when createNewCopies is true
],
@@ -980,12 +1218,15 @@ describe('CoreUsageStatsClient', () => {
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${IMPORT_STATS_PREFIX}.total`,
- `${IMPORT_STATS_PREFIX}.namespace.default.total`,
- `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
- `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
- `${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`,
- `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.yes`,
+ { fieldName: `${IMPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
+ { fieldName: `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.yes`, incrementBy: 1 },
],
incrementOptions
);
@@ -998,17 +1239,18 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsImport({
request,
} as IncrementSavedObjectsImportOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${IMPORT_STATS_PREFIX}.total`,
- `${IMPORT_STATS_PREFIX}.namespace.custom.total`,
- `${IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
- `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
- `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`,
- `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`,
+ { fieldName: `${IMPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, incrementBy: 1 },
+ { fieldName: `${IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`, incrementBy: 1 },
],
incrementOptions
);
@@ -1026,6 +1268,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as IncrementSavedObjectsResolveImportErrorsOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -1036,16 +1279,23 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsResolveImportErrors({
request,
} as IncrementSavedObjectsResolveImportErrorsOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${RESOLVE_IMPORT_STATS_PREFIX}.total`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`,
+ { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
+ { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 },
+ {
+ fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -1060,21 +1310,29 @@ describe('CoreUsageStatsClient', () => {
createNewCopies: true,
compatibilityMode: true,
} as IncrementSavedObjectsResolveImportErrorsOptions);
+ await jest.runOnlyPendingTimersAsync();
await usageStatsClient.incrementSavedObjectsResolveImportErrors({
request,
createNewCopies: false,
compatibilityMode: true,
} as IncrementSavedObjectsResolveImportErrorsOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(2);
expect(repositoryMock.incrementCounter).toHaveBeenNthCalledWith(
1,
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${RESOLVE_IMPORT_STATS_PREFIX}.total`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
+ { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
+ {
+ fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
+ incrementBy: 1,
+ },
// excludes 'compatibilityModeEnabled.yes` and `compatibilityModeEnabled.no` when createNewCopies is true
],
incrementOptions
@@ -1084,11 +1342,17 @@ describe('CoreUsageStatsClient', () => {
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${RESOLVE_IMPORT_STATS_PREFIX}.total`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.yes`,
+ { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
+ { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 },
+ {
+ fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -1101,16 +1365,23 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsResolveImportErrors({
request,
} as IncrementSavedObjectsResolveImportErrorsOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${RESOLVE_IMPORT_STATS_PREFIX}.total`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.total`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
- `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`,
+ { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ {
+ fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ incrementBy: 1,
+ },
+ { fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, incrementBy: 1 },
+ {
+ fieldName: `${RESOLVE_IMPORT_STATS_PREFIX}.compatibilityModeEnabled.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -1128,6 +1399,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as IncrementSavedObjectsExportOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -1140,15 +1412,19 @@ describe('CoreUsageStatsClient', () => {
types: undefined,
supportedTypes: ['foo', 'bar'],
} as IncrementSavedObjectsExportOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${EXPORT_STATS_PREFIX}.total`,
- `${EXPORT_STATS_PREFIX}.namespace.default.total`,
- `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
- `${EXPORT_STATS_PREFIX}.allTypesSelected.no`,
+ { fieldName: `${EXPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${EXPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
+ incrementBy: 1,
+ },
+ { fieldName: `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, incrementBy: 1 },
],
incrementOptions
);
@@ -1163,15 +1439,19 @@ describe('CoreUsageStatsClient', () => {
types: ['foo', 'bar'],
supportedTypes: ['foo', 'bar'],
} as IncrementSavedObjectsExportOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${EXPORT_STATS_PREFIX}.total`,
- `${EXPORT_STATS_PREFIX}.namespace.default.total`,
- `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
- `${EXPORT_STATS_PREFIX}.allTypesSelected.yes`,
+ { fieldName: `${EXPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${EXPORT_STATS_PREFIX}.namespace.default.total`, incrementBy: 1 },
+ {
+ fieldName: `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
+ { fieldName: `${EXPORT_STATS_PREFIX}.allTypesSelected.yes`, incrementBy: 1 },
],
incrementOptions
);
@@ -1184,15 +1464,16 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementSavedObjectsExport({
request,
} as IncrementSavedObjectsExportOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${EXPORT_STATS_PREFIX}.total`,
- `${EXPORT_STATS_PREFIX}.namespace.custom.total`,
- `${EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
- `${EXPORT_STATS_PREFIX}.allTypesSelected.no`,
+ { fieldName: `${EXPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ { fieldName: `${EXPORT_STATS_PREFIX}.namespace.custom.total`, incrementBy: 1 },
+ { fieldName: `${EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, incrementBy: 1 },
+ { fieldName: `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, incrementBy: 1 },
],
incrementOptions
);
@@ -1210,6 +1491,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as IncrementSavedObjectsExportOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -1220,14 +1502,21 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementLegacyDashboardsImport({
request,
} as IncrementSavedObjectsExportOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`,
- `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.total`,
- `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ {
+ fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.total`,
+ incrementBy: 1,
+ },
+ {
+ fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -1240,14 +1529,21 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementLegacyDashboardsImport({
request,
} as IncrementSavedObjectsExportOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`,
- `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.total`,
- `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ {
+ fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.total`,
+ incrementBy: 1,
+ },
+ {
+ fieldName: `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -1265,6 +1561,7 @@ describe('CoreUsageStatsClient', () => {
request,
} as IncrementSavedObjectsExportOptions)
).resolves.toBeUndefined();
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
@@ -1275,14 +1572,21 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementLegacyDashboardsExport({
request,
} as IncrementSavedObjectsExportOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`,
- `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.total`,
- `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ { fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ {
+ fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.total`,
+ incrementBy: 1,
+ },
+ {
+ fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
@@ -1295,14 +1599,21 @@ describe('CoreUsageStatsClient', () => {
await usageStatsClient.incrementLegacyDashboardsExport({
request,
} as IncrementSavedObjectsExportOptions);
+ await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
- `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`,
- `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.total`,
- `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ { fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`, incrementBy: 1 },
+ {
+ fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.total`,
+ incrementBy: 1,
+ },
+ {
+ fieldName: `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
+ incrementBy: 1,
+ },
],
incrementOptions
);
diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts
index f66e51ed2bf77..19c1bc1facafb 100644
--- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts
+++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts
@@ -7,7 +7,10 @@
*/
import type { KibanaRequest, IBasePath } from '@kbn/core-http-server';
-import type { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server';
+import type {
+ ISavedObjectsRepository,
+ SavedObjectsIncrementCounterField,
+} from '@kbn/core-saved-objects-api-server';
import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
import type { CoreUsageStats } from '@kbn/core-usage-data-server';
import {
@@ -20,6 +23,17 @@ import {
CORE_USAGE_STATS_ID,
REPOSITORY_RESOLVE_OUTCOME_STATS,
} from '@kbn/core-usage-data-base-server-internal';
+import {
+ bufferWhen,
+ exhaustMap,
+ filter,
+ interval,
+ map,
+ merge,
+ skip,
+ Subject,
+ takeUntil,
+} from 'rxjs';
export const BULK_CREATE_STATS_PREFIX = 'apiCalls.savedObjectsBulkCreate';
export const BULK_GET_STATS_PREFIX = 'apiCalls.savedObjectsBulkGet';
@@ -74,19 +88,74 @@ const ALL_COUNTER_FIELDS = [
];
const SPACE_CONTEXT_REGEX = /^\/s\/([a-z0-9_\-]+)/;
+// Buffering up to 10k events because:
+// - ALL_COUNTER_FIELDS has 125 fields, so that's the max request we can expect after grouping the keys.
+// - A typical counter reports 3 fields, so taking 10k events, means around 30k fields (to be later grouped into max 125 fields).
+// - Taking into account the longest possible string, this queue can use 15MB max.
+const MAX_BUFFER_SIZE = 10_000;
+const DEFAULT_BUFFER_TIME_MS = 10_000;
+
/** @internal */
export class CoreUsageStatsClient implements ICoreUsageStatsClient {
+ private readonly fieldsToIncrement$ = new Subject();
+ private readonly flush$ = new Subject();
+
constructor(
private readonly debugLogger: (message: string) => void,
private readonly basePath: IBasePath,
- private readonly repositoryPromise: Promise
- ) {}
+ private readonly repositoryPromise: Promise,
+ stop$: Subject,
+ bufferTimeMs: number = DEFAULT_BUFFER_TIME_MS
+ ) {
+ this.fieldsToIncrement$
+ .pipe(
+ takeUntil(stop$),
+ // Buffer until either the timer, a forced flush occur, or there are too many queued fields
+ bufferWhen(() =>
+ merge(
+ interval(bufferTimeMs),
+ this.flush$,
+ this.fieldsToIncrement$.pipe(skip(MAX_BUFFER_SIZE))
+ )
+ ),
+ map((listOfFields) => {
+ const fieldsMap = listOfFields.flat().reduce((acc, fieldName) => {
+ const incrementCounterField: Required = acc.get(
+ fieldName
+ ) ?? {
+ fieldName,
+ incrementBy: 0,
+ };
+ incrementCounterField.incrementBy++;
+ return acc.set(fieldName, incrementCounterField);
+ }, new Map>());
+ return [...fieldsMap.values()];
+ }),
+ filter((fields) => fields.length > 0),
+ exhaustMap(async (fields) => {
+ const options = { refresh: false };
+ try {
+ const repository = await this.repositoryPromise;
+ await repository.incrementCounter(
+ CORE_USAGE_STATS_TYPE,
+ CORE_USAGE_STATS_ID,
+ fields,
+ options
+ );
+ } catch (err) {
+ // do nothing
+ }
+ })
+ )
+ .subscribe();
+ }
public async getUsageStats() {
this.debugLogger('getUsageStats() called');
let coreUsageStats: CoreUsageStats = {};
try {
const repository = await this.repositoryPromise;
+ this.flush$.next();
const result = await repository.incrementCounter(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
@@ -185,19 +254,8 @@ export class CoreUsageStatsClient implements ICoreUsageStatsClient {
prefix: string,
{ request }: BaseIncrementOptions
) {
- const options = { refresh: false };
- try {
- const repository = await this.repositoryPromise;
- const fields = this.getFieldsToIncrement(counterFieldNames, prefix, request);
- await repository.incrementCounter(
- CORE_USAGE_STATS_TYPE,
- CORE_USAGE_STATS_ID,
- fields,
- options
- );
- } catch (err) {
- // do nothing
- }
+ const fields = this.getFieldsToIncrement(counterFieldNames, prefix, request);
+ this.fieldsToIncrement$.next(fields);
}
private getIsDefaultNamespace(request: KibanaRequest) {
From 67ef0a0227e9079af557b6760d68f7df5b526d5f Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Wed, 10 Apr 2024 19:18:46 +0100
Subject: [PATCH 33/55] skip flaky suite (#180499)
---
.../api_integration/apis/transform/reauthorize_transforms.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts b/x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts
index 618af9ef4e3ef..d8d68c6c38bb8 100644
--- a/x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts
+++ b/x-pack/test/api_integration/apis/transform/reauthorize_transforms.ts
@@ -215,7 +215,8 @@ export default ({ getService }: FtrProviderContext) => {
});
});
- describe('bulk reauthorize_transforms', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/180499
+ describe.skip('bulk reauthorize_transforms', function () {
const reqBody: ReauthorizeTransformsRequestSchema = [
USER.TRANSFORM_VIEWER,
USER.TRANSFORM_POWERUSER,
From 11f9cbc2ae9def0a5229eafc5b9ea64bb7a8c336 Mon Sep 17 00:00:00 2001
From: Tiago Costa