diff --git a/package.json b/package.json index 62e47437cda90..704ed9b404a79 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "@elastic/ecs": "^8.11.5", "@elastic/elasticsearch": "9.0.0-alpha.3", "@elastic/ems-client": "8.6.3", - "@elastic/eui": "99.4.0-borealis.0", + "@elastic/eui": "100.0.0", "@elastic/eui-theme-borealis": "0.0.11", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "^1.2.3", diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index 0b0962d1e5069..137466f665260 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -87,7 +87,7 @@ export const LICENSE_OVERRIDES = { 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint '@elastic/ems-client@8.6.3': ['Elastic License 2.0'], - '@elastic/eui@99.4.0-borealis.0': ['Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0'], + '@elastic/eui@100.0.0': ['Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0'], '@elastic/eui-theme-borealis@0.0.11': ['Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0'], 'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry 'buffers@0.1.1': ['MIT'], // license in importing module https://www.npmjs.com/package/binary diff --git a/src/platform/packages/shared/kbn-apm-synthtrace-client/src/lib/otel/index.ts b/src/platform/packages/shared/kbn-apm-synthtrace-client/src/lib/otel/index.ts index 43d159a632203..7ef0ecfdac497 100644 --- a/src/platform/packages/shared/kbn-apm-synthtrace-client/src/lib/otel/index.ts +++ b/src/platform/packages/shared/kbn-apm-synthtrace-client/src/lib/otel/index.ts @@ -131,11 +131,13 @@ class Otel extends Serializable { }, resource: { attributes: { - 'agent.name': 'otlp', + 'agent.name': 'opentelemetry/nodejs', 'agent.version': '1.28.0', 'service.instance.id': '89117ac1-0dbf-4488-9e17-4c2c3b76943a', 'service.name': 'sendotlp-synth', 'metricset.interval': '10m', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.language': 'nodejs', }, dropped_attributes_count: 0, }, diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/index.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/index.ts index d92db7cd9489c..1b9db813d5891 100644 --- a/src/platform/packages/shared/kbn-elastic-agent-utils/index.ts +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/index.ts @@ -32,6 +32,10 @@ export { AGENT_NAMES, } from './src/agent_names'; +export { getIngestionPath } from './src/agent_ingestion_path'; + +export { getSdkNameAndLanguage } from './src/agent_sdk_name_and_language'; + export type { ElasticAgentName, OpenTelemetryAgentName, diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.test.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.test.ts index 1c9360649439b..388c2eebf843d 100644 --- a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.test.ts +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.test.ts @@ -13,6 +13,7 @@ import { isAndroidAgentName, isAWSLambdaAgentName, isAzureFunctionsAgentName, + isElasticAgentName, isIosAgentName, isJavaAgentName, isJRubyAgentName, @@ -44,6 +45,17 @@ describe('Agents guards', () => { expect(isOpenTelemetryAgentName('not-an-agent')).toBe(false); }); + it('isElasticAgentName should guard if the passed agent is an APM agent one.', () => { + expect(isElasticAgentName('nodejs')).toBe(true); + expect(isElasticAgentName('iOS/swift')).toBe(true); + expect(isElasticAgentName('java')).toBe(true); + expect(isElasticAgentName('rum-js')).toBe(true); + expect(isElasticAgentName('android/java')).toBe(true); + expect(isElasticAgentName('node-js')).toBe(false); + expect(isElasticAgentName('opentelemetry/nodejs/elastic')).toBe(false); + expect(isElasticAgentName('not-an-agent')).toBe(false); + }); + it('isJavaAgentName should guard if the passed agent is an Java one.', () => { expect(isJavaAgentName('java')).toBe(true); expect(isJavaAgentName('otlp/java')).toBe(true); diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.ts index ea9d112d9630b..eda96c9853c5d 100644 --- a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.ts +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.ts @@ -9,6 +9,7 @@ import { ANDROID_AGENT_NAMES, + ELASTIC_AGENT_NAMES, IOS_AGENT_NAMES, JAVA_AGENT_NAMES, OPEN_TELEMETRY_AGENT_NAMES, @@ -17,6 +18,7 @@ import { import type { AndroidAgentName, + ElasticAgentName, IOSAgentName, JavaAgentName, OpenTelemetryAgentName, @@ -24,6 +26,8 @@ import type { ServerlessType, } from './agent_names'; +const ElasticAgentNamesSet = new Set(ELASTIC_AGENT_NAMES); + export function getAgentName( agentName: string | null, telemetryAgentName: string | null, @@ -57,6 +61,9 @@ export function isOpenTelemetryAgentName(agentName: string): agentName is OpenTe ); } +export const isElasticAgentName = (agentName: string): agentName is ElasticAgentName => + ElasticAgentNamesSet.has(agentName as ElasticAgentName); + export function isJavaAgentName(agentName?: string): agentName is JavaAgentName { return ( hasOpenTelemetryPrefix(agentName, 'java') || diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_ingestion_path.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_ingestion_path.ts new file mode 100644 index 0000000000000..a41a84792ef67 --- /dev/null +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_ingestion_path.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export const getIngestionPath = (hasOpenTelemetryFields: boolean) => + hasOpenTelemetryFields ? 'otel_native' : 'classic_apm'; diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.test.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.test.ts new file mode 100644 index 0000000000000..9ec09740dcd87 --- /dev/null +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { getSdkNameAndLanguage } from './agent_sdk_name_and_language'; + +describe('getSdkNameAndLanguage', () => { + it.each([ + { + agentName: 'java', + result: { sdkName: 'apm', language: 'java' }, + }, + { + agentName: 'iOS/swift', + result: { sdkName: 'apm', language: 'iOS/swift' }, + }, + { + agentName: 'android/java', + result: { sdkName: 'apm', language: 'android/java' }, + }, + { + agentName: 'opentelemetry/java/test/elastic', + result: { sdkName: 'edot', language: 'java' }, + }, + { + agentName: 'opentelemetry/java/elastic', + result: { sdkName: 'edot', language: 'java' }, + }, + { + agentName: 'otlp/nodejs', + result: { sdkName: 'otel_other', language: 'nodejs' }, + }, + { + agentName: 'otlp', + result: { sdkName: 'otel_other', language: undefined }, + }, + { + agentName: 'test/test/test/something-else/elastic', + result: { sdkName: undefined, language: undefined }, + }, + { + agentName: 'test/java/test/something-else/', + result: { sdkName: undefined, language: undefined }, + }, + { + agentName: 'elastic', + result: { sdkName: undefined, language: undefined }, + }, + { + agentName: 'my-awesome-agent/otel', + result: { sdkName: undefined, language: undefined }, + }, + ])('for the agent name $agentName returns $result', ({ agentName, result }) => { + expect(getSdkNameAndLanguage(agentName)).toStrictEqual(result); + }); +}); diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.ts new file mode 100644 index 0000000000000..1cffd32bcab56 --- /dev/null +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { isElasticAgentName, isOpenTelemetryAgentName } from './agent_guards'; + +interface SdkNameAndLanguage { + sdkName?: 'apm' | 'edot' | 'otel_other'; + language?: string; +} + +const LANGUAGE_INDEX = 1; + +export const getSdkNameAndLanguage = (agentName: string): SdkNameAndLanguage => { + if (isElasticAgentName(agentName)) { + return { sdkName: 'apm', language: agentName }; + } + const agentNameParts = agentName.split('/'); + + if (isOpenTelemetryAgentName(agentName)) { + if (agentNameParts[agentNameParts.length - 1] === 'elastic') { + return { sdkName: 'edot', language: agentNameParts[LANGUAGE_INDEX] }; + } + return { + sdkName: 'otel_other', + language: agentNameParts[LANGUAGE_INDEX], + }; + } + + return { sdkName: undefined, language: undefined }; +}; diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/xy/persistence.ts b/x-pack/platform/plugins/shared/lens/public/visualizations/xy/persistence.ts index 8ad5d82d364e2..3d96a7be03d36 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/xy/persistence.ts +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/xy/persistence.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { v4 as uuidv4 } from 'uuid'; import type { SavedObjectReference } from '@kbn/core/public'; import { EVENT_ANNOTATION_GROUP_TYPE } from '@kbn/event-annotation-common'; import { cloneDeep } from 'lodash'; @@ -100,58 +99,68 @@ export function convertToPersistable(state: XYState) { const persistableLayers: XYPersistedLayerConfig[] = []; persistableState.layers.forEach((layer) => { + // anything but an annotation can just be persisted as is if (!isAnnotationsLayer(layer)) { persistableLayers.push(layer); - } else { - if (isByReferenceAnnotationsLayer(layer)) { - const referenceName = `ref-${uuidv4()}`; - savedObjectReferences.push({ - type: EVENT_ANNOTATION_GROUP_TYPE, - id: layer.annotationGroupId, - name: referenceName, - }); - - if (!annotationLayerHasUnsavedChanges(layer)) { - const persistableLayer: XYPersistedByReferenceAnnotationLayerConfig = { - persistanceType: 'byReference', - layerId: layer.layerId, - layerType: layer.layerType, - annotationGroupRef: referenceName, - }; - - persistableLayers.push(persistableLayer); - } else { - const persistableLayer: XYPersistedLinkedByValueAnnotationLayerConfig = { - persistanceType: 'linked', - cachedMetadata: layer.cachedMetadata || { - title: layer.__lastSaved.title, - description: layer.__lastSaved.description, - tags: layer.__lastSaved.tags, - }, - layerId: layer.layerId, - layerType: layer.layerType, - annotationGroupRef: referenceName, - annotations: layer.annotations, - ignoreGlobalFilters: layer.ignoreGlobalFilters, - }; - persistableLayers.push(persistableLayer); - - savedObjectReferences.push({ - type: 'index-pattern', - id: layer.indexPatternId, - name: getLayerReferenceName(layer.layerId), - }); - } - } else { - const { indexPatternId, ...persistableLayer } = layer; - savedObjectReferences.push({ - type: 'index-pattern', - id: indexPatternId, - name: getLayerReferenceName(layer.layerId), - }); - persistableLayers.push({ ...persistableLayer, persistanceType: 'byValue' }); - } + return; } + // a by value annotation layer can be persisted with some config tweak + if (!isByReferenceAnnotationsLayer(layer)) { + const { indexPatternId, ...persistableLayer } = layer; + savedObjectReferences.push({ + type: 'index-pattern', + id: indexPatternId, + name: getLayerReferenceName(layer.layerId), + }); + persistableLayers.push({ ...persistableLayer, persistanceType: 'byValue' }); + return; + } + /** + * by reference annotation layer needs to be handled carefully + **/ + + // make this id stable so that it won't retrigger all the time a change diff + const referenceName = `ref-${layer.layerId}`; + savedObjectReferences.push({ + type: EVENT_ANNOTATION_GROUP_TYPE, + id: layer.annotationGroupId, + name: referenceName, + }); + + // if there's no divergence from the library, it can be persisted without much ado + if (!annotationLayerHasUnsavedChanges(layer)) { + const persistableLayer: XYPersistedByReferenceAnnotationLayerConfig = { + persistanceType: 'byReference', + layerId: layer.layerId, + layerType: layer.layerType, + annotationGroupRef: referenceName, + }; + + persistableLayers.push(persistableLayer); + return; + } + // this is the case where the by reference diverged from library + // so it needs to persist some extra metadata + const persistableLayer: XYPersistedLinkedByValueAnnotationLayerConfig = { + persistanceType: 'linked', + cachedMetadata: layer.cachedMetadata || { + title: layer.__lastSaved.title, + description: layer.__lastSaved.description, + tags: layer.__lastSaved.tags, + }, + layerId: layer.layerId, + layerType: layer.layerType, + annotationGroupRef: referenceName, + annotations: layer.annotations, + ignoreGlobalFilters: layer.ignoreGlobalFilters, + }; + persistableLayers.push(persistableLayer); + + savedObjectReferences.push({ + type: 'index-pattern', + id: layer.indexPatternId, + name: getLayerReferenceName(layer.layerId), + }); }); return { savedObjectReferences, state: { ...persistableState, layers: persistableLayers } }; } diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/swimlane_container.tsx b/x-pack/platform/plugins/shared/ml/public/application/explorer/swimlane_container.tsx index b0bd6270c205f..4b4ffbe7ff37a 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/swimlane_container.tsx @@ -31,15 +31,7 @@ import type { TooltipProps, TooltipValue, } from '@elastic/charts'; -import { - Chart, - Heatmap, - Position, - ScaleType, - Settings, - Tooltip, - LEGACY_LIGHT_THEME, -} from '@elastic/charts'; +import { Chart, Heatmap, Position, ScaleType, Settings, Tooltip } from '@elastic/charts'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; @@ -200,6 +192,10 @@ export const SwimlaneContainer: FC = ({ yAxisWidth, onRenderComplete, }) => { + const { + theme: { useChartsBaseTheme }, + } = chartsService; + const [chartWidth, setChartWidth] = useState(0); const { colorMode, euiTheme } = useEuiTheme(); @@ -218,6 +214,8 @@ export const SwimlaneContainer: FC = ({ [chartWidth] ); + const baseTheme = useChartsBaseTheme(); + const swimLanePoints = useMemo(() => { const showFilterContext = filterActive === true && swimlaneType === SWIMLANE_TYPE.OVERALL; @@ -458,8 +456,7 @@ export const SwimlaneContainer: FC = ({ {} } }, +} as unknown as Partial); + +function MetricsWithWrapper() { + jest + .spyOn(useApmDataViewHook, 'useAdHocApmDataView') + .mockReturnValue({ dataView: { id: 'id-1', name: 'apm-data-view' } as DataView }); + + const history = createMemoryHistory(); + history.replace({ + pathname: '/services/testServiceName/metrics', + search: fromQuery({ + rangeFrom: 'now-15m', + rangeTo: 'now', + }), + }); + + return ( + + + + + + ); +} + +describe('Metrics', () => { + describe('render the correct metrics content for', () => { + describe('APM agent / server service', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'java', + serviceName: 'testServiceName', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummary: { + dataStreamTypes: ['metrics'], + } as unknown as ServiceEntitySummary, + }); + }); + + it('shows java dashboard content', () => { + const result = render(); + // Check that the other content is not rendered as we don't have test id in the dashboard rendering component + const loadingBar = result.queryByRole('progressbar'); + expect(loadingBar).toBeNull(); + expect(result.queryByTestId('apmMetricsNoDashboardFound')).toBeNull(); + expect(result.queryByTestId('apmAddApmCallout')).toBeNull(); + }); + }); + + describe('APM agent / EDOT sdk with dashboard', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'opentelemetry/nodejs/elastic', + serviceName: 'testServiceName', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummary: { + dataStreamTypes: ['metrics'], + } as unknown as ServiceEntitySummary, + }); + }); + + it('shows nodejs dashboard content', () => { + const result = render(); + // Check that the other content is not rendered as we don't have test id in the dashboard rendering component + const loadingBar = result.queryByRole('progressbar'); + expect(loadingBar).toBeNull(); + expect(result.queryByTestId('apmMetricsNoDashboardFound')).toBeNull(); + expect(result.queryByTestId('apmAddApmCallout')).toBeNull(); + }); + }); + + describe('APM agent / otel sdk with no dashboard', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'opentelemetry/go', + serviceName: 'testServiceName', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummary: { + dataStreamTypes: ['metrics'], + } as unknown as ServiceEntitySummary, + }); + }); + + it('shows "no dashboard found" message', () => { + const result = render(); + const apmMetricsNoDashboardFound = result.getByTestId('apmMetricsNoDashboardFound'); + expect(apmMetricsNoDashboardFound).toBeInTheDocument(); + }); + }); + + describe('Logs signals', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'java', + serviceName: 'testServiceName', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummary: { + dataStreamTypes: ['logs'], + } as unknown as ServiceEntitySummary, + }); + }); + + it('shows service from logs metrics content', () => { + const result = render(); + const apmAddApmCallout = result.getByTestId('apmAddApmCallout'); + expect(apmAddApmCallout).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/index.tsx index e89bffeaf5b5d..06fdee55d4cfe 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/index.tsx @@ -6,27 +6,33 @@ */ import React from 'react'; -import { - isJavaAgentName, - isJRubyAgentName, - isAWSLambdaAgentName, -} from '../../../../common/agent_name'; +import { isElasticAgentName, isJRubyAgentName } from '@kbn/elastic-agent-utils/src/agent_guards'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isAWSLambdaAgentName } from '../../../../common/agent_name'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { ServerlessMetrics } from './serverless_metrics'; import { ServiceMetrics } from './service_metrics'; -import { JvmMetricsOverview } from './jvm_metrics_overview'; import { JsonMetricsDashboard } from './static_dashboard'; -import { hasDashboardFile } from './static_dashboard/helper'; +import { hasDashboard } from './static_dashboard/helper'; import { useAdHocApmDataView } from '../../../hooks/use_adhoc_apm_data_view'; import { isLogsOnlySignal } from '../../../utils/get_signal_type'; import { ServiceTabEmptyState } from '../service_tab_empty_state'; +import { JvmMetricsOverview } from './jvm_metrics_overview'; export function Metrics() { - const { agentName, runtimeName, serverlessType } = useApmServiceContext(); + const { + agentName, + runtimeName, + serverlessType, + serviceEntitySummary, + telemetrySdkName, + telemetrySdkLanguage, + } = useApmServiceContext(); const isAWSLambda = isAWSLambdaAgentName(serverlessType); const { dataView } = useAdHocApmDataView(); - const { serviceEntitySummary } = useApmServiceContext(); + const hasDashboardFile = hasDashboard({ agentName, telemetrySdkName, telemetrySdkLanguage }); const hasLogsOnlySignal = serviceEntitySummary?.dataStreamTypes && isLogsOnlySignal(serviceEntitySummary.dataStreamTypes); @@ -38,13 +44,19 @@ export function Metrics() { return ; } - const hasStaticDashboard = hasDashboardFile({ - agentName, - runtimeName, - serverlessType, - }); + if (!hasDashboardFile && !isElasticAgentName(agentName ?? '')) { + return ( + + ); + } - if (hasStaticDashboard && dataView) { + if (hasDashboardFile && dataView) { return ( ; } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts index ea3c468a6c829..59ba771dea86c 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts @@ -5,52 +5,69 @@ * 2.0. */ -export const AGENT_NAME_DASHBOARD_FILE_MAPPING: Record = { - nodejs: 'nodejs', - 'opentelemetry/nodejs': 'opentelemetry_nodejs', - 'opentelemetry/nodejs/elastic': 'opentelemetry_nodejs', - java: 'java', - 'opentelemetry/java': 'opentelemetry_java', - 'opentelemetry/java/opentelemetry-java-instrumentation': 'opentelemetry_java', - 'opentelemetry/java/elastic': 'opentelemetry_java', - 'opentelemetry/dotnet': 'opentelemetry_dotnet', - 'opentelemetry/dotnet/opentelemetry-dotnet-instrumentation': 'opentelemetry_dotnet', - 'opentelemetry/dotnet/elastic': 'opentelemetry_dotnet', -}; +// The new dashboard file names should be added here +export const existingDashboardFileNames = new Set([ + 'classic_apm-apm-nodejs', + 'classic_apm-apm-java', + 'classic_apm-otel_other-nodejs', + 'classic_apm-otel_other-java', + 'classic_apm-otel_other-dotnet', + 'classic_apm-edot-nodejs', + 'classic_apm-edot-java', + 'classic_apm-edot-dotnet', +]); -/** - * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. - * See https://webpack.js.org/api/module-methods/#magic-comments - */ -export async function loadDashboardFile(filename: string): Promise { +// The new dashboard files should be mapped here +// + changed with the new ones (following the naming convention) +// + similar mapping for edot needed +// - example: otel_native-edot-nodejs +export async function loadDashboardFile(filename: string) { switch (filename) { - case 'nodejs': { + case 'classic_apm-apm-nodejs': { return import( - /* webpackChunkName: "lazyNodeJsDashboard" */ + /* webpackChunkName: "lazyNodeJsClassicApmDashboard" */ './nodejs.json' ); } - case 'opentelemetry_nodejs': { + case 'classic_apm-otel_other-nodejs': { return import( - /* webpackChunkName: "lazyNodeJsDashboard" */ + /* webpackChunkName: "lazyNodeJsApmOtelDashboard" */ './opentelemetry_nodejs.json' ); } - case 'java': { + case 'classic_apm-edot-nodejs': { return import( - /* webpackChunkName: "lazyJavaDashboard" */ + /* webpackChunkName: "lazyNodeJsOtelNativeDashboard" */ + './opentelemetry_nodejs.json' + ); + } + case 'classic_apm-apm-java': { + return import( + /* webpackChunkName: "lazyJavaClassicApmDashboard" */ './java.json' ); } - case 'opentelemetry_java': { + case 'classic_apm-otel_other-java': { return import( - /* webpackChunkName: "lazyJavaDashboard" */ + /* webpackChunkName: "lazyJavaApmOtelDashboard" */ './opentelemetry_java.json' ); } - case 'opentelemetry_dotnet': { + case 'classic_apm-edot-java': { + return import( + /* webpackChunkName: "lazyJavaOtelNativeDashboard" */ + './opentelemetry_java.json' + ); + } + case 'classic_apm-edot-dotnet': { + return import( + /* webpackChunkName: "lazyDotnetOtelNativeDashboard" */ + './opentelemetry_dotnet.json' + ); + } + case 'classic_apm-otel_other-dotnet': { return import( - /* webpackChunkName: "lazyOtelDotnetDashboard" */ + /* webpackChunkName: "lazyDotnetApmOtelDashboard" */ './opentelemetry_dotnet.json' ); } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.test.ts b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.test.ts new file mode 100644 index 0000000000000..5043ff98ac855 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { getDashboardFileName } from './get_dashboard_file_name'; + +const apmAgent = [ + { + agentName: 'java', + telemetrySdkName: undefined, + telemetrySdkLanguage: undefined, + filename: 'classic_apm-apm-java', + }, + { + agentName: 'iOS/swift', + telemetrySdkName: undefined, + telemetrySdkLanguage: undefined, + filename: 'classic_apm-apm-ios_swift', + }, + { + agentName: 'java', + telemetrySdkName: 'opentelemetry', + filename: 'otel_native-apm-java', + }, +]; +const edotSdk = [ + { + agentName: 'opentelemetry/java/test/elastic', + filename: 'classic_apm-edot-java', + }, + { + agentName: 'opentelemetry/java/elastic', + filename: 'classic_apm-edot-java', + }, + { + agentName: 'opentelemetry/java/test/elastic', + filename: 'classic_apm-edot-java', + }, + { + agentName: 'opentelemetry/java/elastic', + filename: 'classic_apm-edot-java', + }, + { + agentName: 'opentelemetry/java/elastic', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'java', + filename: 'otel_native-edot-java', + }, + { + agentName: 'opentelemetry/nodejs/nodejs-agent/elastic', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'nodejs', + filename: 'otel_native-edot-nodejs', + }, +]; +const vanillaOtelSdk = [ + { + agentName: 'opentelemetry/java', + filename: 'classic_apm-otel_other-java', + }, + { + agentName: 'opentelemetry/nodejs/test/nodejs-agent', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'nodejs', + filename: 'otel_native-otel_other-nodejs', + }, + { + agentName: 'opentelemetry/java/test/something-else/', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'java', + filename: 'otel_native-otel_other-java', + }, + { + agentName: 'otlp/nodejs', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'nodejs', + filename: 'otel_native-otel_other-nodejs', + }, + { + agentName: 'otlp/Android', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'android', + filename: 'otel_native-otel_other-android', + }, +]; +const noFilenameCases = [ + { + agentName: 'test/java/test/something-else/', + telemetrySdkName: undefined, + telemetrySdkLanguage: undefined, + filename: undefined, + }, + { + agentName: 'otlp', + filename: undefined, + }, + { + agentName: 'elastic', + filename: undefined, + }, + { + agentName: 'my-awesome-agent/otel', + telemetrySdkName: 'opentelemetry', + filename: undefined, + }, +]; + +describe('getDashboardFileName', () => { + describe('apmAgent', () => { + it.each(apmAgent)( + 'for the agent name $agentName and open telemetry sdk name: $telemetrySdkName returns $filename', + ({ agentName, telemetrySdkName, telemetrySdkLanguage, filename }) => { + expect( + getDashboardFileName({ agentName, telemetrySdkName, telemetrySdkLanguage }) + ).toStrictEqual(filename); + } + ); + }); + describe('vanillaOtelSdk', () => { + it.each(vanillaOtelSdk)( + 'for the agent name $agentName and open telemetry sdk name: $telemetrySdkName and language $telemetrySdkLanguage returns $filename', + ({ agentName, telemetrySdkName, telemetrySdkLanguage, filename }) => { + expect( + getDashboardFileName({ agentName, telemetrySdkName, telemetrySdkLanguage }) + ).toStrictEqual(filename); + } + ); + }); + describe('edotSdk', () => { + it.each(edotSdk)( + 'for the agent name $agentName and open telemetry sdk name: $telemetrySdkName and language $telemetrySdkLanguage returns $filename', + ({ agentName, telemetrySdkName, telemetrySdkLanguage, filename }) => { + expect( + getDashboardFileName({ agentName, telemetrySdkName, telemetrySdkLanguage }) + ).toStrictEqual(filename); + } + ); + }); + describe('noFilenameCases', () => { + it.each(noFilenameCases)( + 'for the agent name $agentName and open telemetry sdk name: $telemetrySdkName and language $telemetrySdkLanguage returns $filename', + ({ agentName, telemetrySdkName, telemetrySdkLanguage, filename }) => { + expect( + getDashboardFileName({ agentName, telemetrySdkName, telemetrySdkLanguage }) + ).toStrictEqual(filename); + } + ); + }); +}); diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.ts b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.ts new file mode 100644 index 0000000000000..30debf725fe88 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.ts @@ -0,0 +1,33 @@ +/* + * 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 { getSdkNameAndLanguage, getIngestionPath } from '@kbn/elastic-agent-utils'; + +interface DashboardFileNamePartsProps { + agentName: string; + telemetrySdkName?: string; + telemetrySdkLanguage?: string; +} + +// We use the language name in the filename so we want to have a valid filename +// Example swift/iOS -> swift_ios : lowercased and '/' is replaces by '_' +const standardizeLanguageName = (languageName?: string) => + languageName ? languageName.toLowerCase().replace('/', '_') : undefined; + +export const getDashboardFileName = ({ + agentName, + telemetrySdkName, + telemetrySdkLanguage, +}: DashboardFileNamePartsProps): string | undefined => { + const dataFormat = getIngestionPath(!!(telemetrySdkName ?? telemetrySdkLanguage)); + const { sdkName, language } = getSdkNameAndLanguage(agentName); + const sdkLanguage = standardizeLanguageName(language); + if (!sdkName || !sdkLanguage) { + return undefined; + } + return `${dataFormat}-${sdkName}-${sdkLanguage}`; +}; diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts index 2674034cd3d7e..a2b92f2b6a6ff 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts @@ -7,28 +7,33 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import type { DashboardPanelMap } from '@kbn/dashboard-plugin/common'; -import { - AGENT_NAME_DASHBOARD_FILE_MAPPING, - loadDashboardFile, -} from './dashboards/dashboard_catalog'; - +import { existingDashboardFileNames, loadDashboardFile } from './dashboards/dashboard_catalog'; +import { getDashboardFileName } from './dashboards/get_dashboard_file_name'; interface DashboardFileProps { agentName?: string; runtimeName?: string; serverlessType?: string; + telemetrySdkName?: string; + telemetrySdkLanguage?: string; } export interface MetricsDashboardProps extends DashboardFileProps { dataView: DataView; } -export function hasDashboardFile(props: DashboardFileProps) { - return !!getDashboardFileName(props); +function getDashboardFileNameFromProps({ + agentName, + telemetrySdkName, + telemetrySdkLanguage, +}: DashboardFileProps) { + const dashboardFile = + agentName && getDashboardFileName({ agentName, telemetrySdkName, telemetrySdkLanguage }); + return dashboardFile; } -function getDashboardFileName({ agentName }: DashboardFileProps) { - const dashboardFile = agentName && AGENT_NAME_DASHBOARD_FILE_MAPPING[agentName]; - return dashboardFile; +export function hasDashboard(props: DashboardFileProps) { + const dashboardFilename = getDashboardFileNameFromProps(props); + return !!dashboardFilename && existingDashboardFileNames.has(dashboardFilename); } const getAdhocDataView = (dataView: DataView) => { @@ -43,10 +48,8 @@ export async function convertSavedDashboardToPanels( props: MetricsDashboardProps, dataView: DataView ): Promise { - const dashboardFilename = getDashboardFileName(props); - const dashboardJSON = !!dashboardFilename - ? await loadDashboardFile(dashboardFilename) - : undefined; + const dashboardFilename = getDashboardFileNameFromProps(props); + const dashboardJSON = !!dashboardFilename ? await loadDashboardFile(dashboardFilename) : false; if (!dashboardFilename || !dashboardJSON) { return undefined; diff --git a/x-pack/solutions/observability/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/solutions/observability/plugins/apm/public/context/apm_service/apm_service_context.tsx index e0c09c5756a4e..3f4ae1a2fa1d2 100644 --- a/x-pack/solutions/observability/plugins/apm/public/context/apm_service/apm_service_context.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -28,6 +28,8 @@ import { export interface APMServiceContextValue { serviceName: string; agentName?: string; + telemetrySdkName?: string; + telemetrySdkLanguage?: string; serverlessType?: ServerlessType; transactionType?: string; transactionTypeStatus: FETCH_STATUS; @@ -63,6 +65,8 @@ export function ApmServiceContextProvider({ children }: { children: ReactNode }) agentName, runtimeName, serverlessType, + telemetrySdkName, + telemetrySdkLanguage, status: serviceAgentStatus, } = useServiceAgentFetcher({ serviceName, @@ -108,6 +112,8 @@ export function ApmServiceContextProvider({ children }: { children: ReactNode }) serviceName, agentName, serverlessType, + telemetrySdkName, + telemetrySdkLanguage, transactionType: currentTransactionType, transactionTypeStatus, transactionTypes, diff --git a/x-pack/solutions/observability/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts b/x-pack/solutions/observability/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts index a7d22e24e1a72..909ba6b36bdaa 100644 --- a/x-pack/solutions/observability/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts +++ b/x-pack/solutions/observability/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts @@ -11,6 +11,8 @@ const INITIAL_STATE = { agentName: undefined, runtimeName: undefined, serverlessType: undefined, + telemetrySdkName: undefined, + telemetrySdkLanguage: undefined, }; export function useServiceAgentFetcher({ diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/services/get_service_agent.ts b/x-pack/solutions/observability/plugins/apm/server/routes/services/get_service_agent.ts index c1ea959935006..812d6e5ceda59 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/services/get_service_agent.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/services/get_service_agent.ts @@ -15,6 +15,8 @@ import { SERVICE_RUNTIME_NAME, CLOUD_PROVIDER, CLOUD_SERVICE_NAME, + TELEMETRY_SDK_NAME, + TELEMETRY_SDK_LANGUAGE, } from '../../../common/es_fields/apm'; import type { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; import type { ServerlessType } from '../../../common/serverless'; @@ -24,6 +26,8 @@ import { maybe } from '../../../common/utils/maybe'; export interface ServiceAgentResponse { agentName?: string; runtimeName?: string; + telemetrySdkName?: string; + telemetrySdkLanguage?: string; serverlessType?: ServerlessType; } @@ -40,6 +44,8 @@ export async function getServiceAgent({ }): Promise { const fields = asMutableArray([ AGENT_NAME, + TELEMETRY_SDK_NAME, + TELEMETRY_SDK_LANGUAGE, SERVICE_RUNTIME_NAME, CLOUD_PROVIDER, CLOUD_SERVICE_NAME, @@ -48,7 +54,12 @@ export async function getServiceAgent({ const params = { terminate_after: 1, apm: { - events: [ProcessorEvent.error, ProcessorEvent.transaction, ProcessorEvent.metric], + events: [ + ProcessorEvent.span, + ProcessorEvent.error, + ProcessorEvent.transaction, + ProcessorEvent.metric, + ], }, track_total_hits: 1, size: 1, @@ -97,11 +108,13 @@ export async function getServiceAgent({ const event = unflattenKnownApmEventFields(hit.fields); - const { agent, service, cloud } = event; + const { agent, service, cloud, telemetry } = event; const serverlessType = getServerlessTypeFromCloudData(cloud?.provider, cloud?.service?.name); return { agentName: agent?.name, + telemetrySdkName: telemetry?.sdk?.name, + telemetrySdkLanguage: telemetry?.sdk?.language, runtimeName: service?.runtime?.name, serverlessType, }; diff --git a/x-pack/solutions/observability/plugins/observability/dev_docs/rules_alerts_cases_overviewpage.md b/x-pack/solutions/observability/plugins/observability/dev_docs/rules_alerts_cases_overviewpage.md new file mode 100644 index 0000000000000..0356ed1d18e3e --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/dev_docs/rules_alerts_cases_overviewpage.md @@ -0,0 +1,125 @@ + +# Test plan for: Rules, Alerts, Overview page, and Cases + +This plan will cover the UI part (not API) for: + +- Rules +- Alerts +- Overview page +- Cases + +## Data generation + +> [!WARNING] +> This guide will not cover how to run Kibana and ES locally. It assumes both instances are running before starting the data ingestion + +> [!TIP] +> The following commands use [synthtrace](https://github.com/elastic/kibana/blob/main/packages/kbn-apm-synthtrace/README.md) to generate some data that will be used to test the rules. +Synthtrace has many scenarios, any of them could work as long as it has data that makes sense with the rules that need to be tested. + +For this test will use `logs_traces_hosts` scenario. + +``` +node scripts/synthtrace.js logs_traces_hosts --live +``` + +That will generate data in +`metrics-system*,metrics-kubernetes*,metrics-docker*,metrics-aws*`, +`traces-apm*,metrics-apm*,logs-apm*`, +`logs-*-*,cloud-logs-*-*` + +> [!NOTE] +> The above indices will be used later to create the rules' data views. + +## Rules and alerts + +### Creation + +- Login to Kibana +- Form Kibana side nav click `Alerts` under `Observability` +- Click `Manage rules` on the right top side. +- Click `Create Rule` button on the right top side. +- From the `Select rule type` modal select (Repeat this part for each rule). The above mentioned indices would be used either to create a data view or as index pattern: + - Custom Threshold + - Metric Threshold + - Inventory Threshold + - APM Latency threshold + - APM Error count + - APM Failed transaction rate + - Anomaly (it's hard to setup up locally as it relies on ML. Use oblt-cli instead) + - ESQ + - Logs threshold + +> [!NOTE] +> +> - Create rules with and without groupBy. +> - Create rules with and without filters. +> - Create rules with different types of aggregations (if applicable). +> - Create rules with check and unchecked Alert on no data. +> - Create rules with Actions (Log connector), and check if the action variables are logged. + +### Rule details and rules actions + +Visit the rule details page, then check and test: + +- Alerts table + - UI filter + - Search bar + - Time range +- Execution history with response filter and time range +- Alert activity by clicking on "Active now" and how that reflects on the alert table +- Enable/Disable the rule. +- Snooze the rule +- Edit the rule + +### Alerts + +Visit the alerts page, then check and test: + +- Alert table + - Update the columns and fields + - Change the time range + - Use __Group alerts by__ option + - Change Table density and lines per row + - Use `Actions` menu items (Add to exciting and new case, View rule details, etc.) +- Use the UI filters (Show all, Active, Recovered, and Untracked) and see how it reflects the results in the alert table +- Use the Search bar and see how it reflects the results in the alert table +- Click on the Disabled and Snoozed at the top, it should lead to the rules page with the right filters + +## Overview page + +Visit the Overview page under Observability, then check and test: + +- The 4 sections (Alerts, Log Events, Hosts, and Services) are visible and show data. +- Change the time range at the top of the page, it should change the data shown in the 4 sections. +- Try the links of each section: "Show alerts", "Show logs", "Show inventory", and "Show service inventory". + +> [!NOTE] +> +> If there is not data showing, check if the data generation is working, and the time range is correct. + +## Cases + +Visit the Cases page under Observability, then check and test: + +- Create many cases with different types of Severity and description +- Cases table + - Check all cases are visible + - Apply filters and check how it reflects on the cases table +- Cases + - Open one case + - Add comments and change the Sort by. + - Click on "Mark in progress" + - Edit the fields in the right side panel + - Change the status from the status drop down-menu at the top left. + - Open the `Files` tab and add a file + - Download it + - Delete it + - Open the `Observables` tab and add an Observable e.g host, IP + - Edit it + - Delete it +From the Alerts table, go to the Alerts page + - On an alert click the more button `...` in the alert table, then click `Add to existing case` + - On an alert click the more button `...` in the alert table, then click `Add to new case` + - Click on the toast that appears after each action and verify that the alert was added correctly + - Open the `Alerts` tab and check all the added alerts to that case diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/manage_custom_pipeline_actions.tsx b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/manage_custom_pipeline_actions.tsx index 9d30e9f949c90..25e35e05fe034 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/manage_custom_pipeline_actions.tsx +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/manage_custom_pipeline_actions.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useState, MutableRefObject } from 'react'; import { css } from '@emotion/react'; import { useActions } from 'kea'; @@ -20,7 +20,11 @@ const revertContextMenuItemCSS = css` color: ${euiThemeVars.euiColorDanger}; `; -export const ManageCustomPipelineActions: React.FC = () => { +interface ManageCustomPipelineProps { + buttonRef: MutableRefObject; +} + +export const ManageCustomPipelineActions: React.FC = ({ buttonRef }) => { const [isMenuOpen, setIsMenuOpen] = useState(false); const { openDeleteModal } = useActions(PipelinesLogic); @@ -32,7 +36,13 @@ export const ManageCustomPipelineActions: React.FC = () => { return ( + {i18n.translate( 'xpack.enterpriseSearch.content.indices.pipelines.ingestionPipeline.manageButton', { defaultMessage: 'Manage' } diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx index 9b60c2dff5c10..49a6e6c7db276 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useActions, useValues } from 'kea'; @@ -33,6 +33,7 @@ import { RevertConnectorPipelineApilogic } from '../../../api/pipelines/revert_c import { getContentExtractionDisabled, isApiIndex, isConnectorIndex } from '../../../utils/indices'; import { IndexNameLogic } from '../index_name_logic'; +import { SearchIndexTabId } from '../search_index'; import { InferenceErrors } from './inference_errors'; import { InferenceHistory } from './inference_history'; @@ -65,6 +66,29 @@ export const SearchIndexPipelines: React.FC = () => { const { makeRequest: revertPipeline } = useActions(RevertConnectorPipelineApilogic); const apiIndex = isApiIndex(index); const extractionDisabled = getContentExtractionDisabled(index); + const [isRevertPipeline, setRevertPipeline] = useState(false); + const buttonRef = useRef(null); + + useEffect(() => { + if (!isDeleteModalOpen) { + if (isRevertPipeline) { + const pipelinesButton = document.querySelector( + `[id="${SearchIndexTabId.PIPELINES}"]` + ); + if (pipelinesButton) { + pipelinesButton.focus(); + } + setRevertPipeline(false); + } else if (buttonRef.current) { + buttonRef.current.focus(); + } + } + }, [isDeleteModalOpen]); + + const onDeletePipeline = useCallback(() => { + revertPipeline({ indexName }); + setRevertPipeline(true); + }, [indexName, revertPipeline]); useEffect(() => { if (index) { @@ -193,7 +217,7 @@ export const SearchIndexPipelines: React.FC = () => { - + ) : ( @@ -280,7 +304,7 @@ export const SearchIndexPipelines: React.FC = () => { )} isLoading={revertStatus === Status.LOADING} onCancel={closeDeleteModal} - onConfirm={() => revertPipeline({ indexName })} + onConfirm={onDeletePipeline} cancelButtonText={CANCEL_BUTTON_LABEL} confirmButtonText={i18n.translate( 'xpack.enterpriseSearch.content.index.pipelines.deleteModal.confirmButton', diff --git a/x-pack/solutions/search/plugins/serverless_search/public/application/components/connectors/self_managed_connectors_empty_prompt.tsx b/x-pack/solutions/search/plugins/serverless_search/public/application/components/connectors/self_managed_connectors_empty_prompt.tsx index 6ae33eac16074..3deb53472e5fb 100644 --- a/x-pack/solutions/search/plugins/serverless_search/public/application/components/connectors/self_managed_connectors_empty_prompt.tsx +++ b/x-pack/solutions/search/plugins/serverless_search/public/application/components/connectors/self_managed_connectors_empty_prompt.tsx @@ -35,16 +35,13 @@ export const SelfManagedConnectorsEmptyPrompt: React.FC = () => { return ( { - + diff --git a/x-pack/solutions/search/plugins/serverless_search/public/application/components/connectors_overview.tsx b/x-pack/solutions/search/plugins/serverless_search/public/application/components/connectors_overview.tsx index 42430df155e3d..c265e4c60be11 100644 --- a/x-pack/solutions/search/plugins/serverless_search/public/application/components/connectors_overview.tsx +++ b/x-pack/solutions/search/plugins/serverless_search/public/application/components/connectors_overview.tsx @@ -7,17 +7,15 @@ import { EuiButton, - EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink, EuiPageTemplate, - EuiSpacer, EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { GithubLink } from '@kbn/search-api-panels'; import { SelfManagedConnectorsEmptyPrompt } from './connectors/self_managed_connectors_empty_prompt'; @@ -29,9 +27,6 @@ import { useKibanaServices } from '../hooks/use_kibana'; import { ConnectorsTable } from './connectors/connectors_table'; import { ConnectorPrivilegesCallout } from './connectors/connector_config/connector_privileges_callout'; import { useAssetBasePath } from '../hooks/use_asset_base_path'; -import { BASE_CONNECTORS_PATH, ELASTIC_MANAGED_CONNECTOR_PATH } from '../constants'; - -const CALLOUT_KEY = 'search.connectors.ElasticManaged.ComingSoon.feedbackCallout'; export const ConnectorsOverview = () => { const { data, isLoading: connectorsLoading } = useConnectors(); @@ -42,14 +37,7 @@ export const ConnectorsOverview = () => { [consolePlugin] ); const canManageConnectors = !data || data.canManageConnectors; - const { - application: { navigateToUrl }, - } = useKibanaServices(); - const [showCallOut, setShowCallOut] = useState(sessionStorage.getItem(CALLOUT_KEY) !== 'hidden'); - const onDismiss = () => { - setShowCallOut(false); - sessionStorage.setItem(CALLOUT_KEY, 'hidden'); - }; + const assetBasePath = useAssetBasePath(); return ( @@ -116,39 +104,7 @@ export const ConnectorsOverview = () => { {connectorsLoading || (data?.connectors || []).length > 0 ? ( - <> - {showCallOut && ( - <> - -

- {i18n.translate( - 'xpack.serverlessSearch.connectorsOverview.calloutDescription', - { - defaultMessage: - "We're actively developing Elastic managed connectors, that won't require any self-managed infrastructure. You'll be able to handle all configuration in the UI. This will simplify syncing your data into a serverless Elasticsearch project.", - } - )} -

- - navigateToUrl(`${BASE_CONNECTORS_PATH}/${ELASTIC_MANAGED_CONNECTOR_PATH}`) - } - > - {LEARN_MORE_LABEL} - -
- - - )} - - + ) : ( )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.tsx index 9d2f3f521f6c1..4e84c75621f63 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.tsx @@ -14,6 +14,7 @@ import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { DataViewBase } from '@kbn/es-query'; import { buildEsQuery } from '@kbn/es-query'; import { TableId } from '@kbn/securitysolution-data-table'; +import { AlertTableCellContextProvider } from '../../../../detections/configurations/security_solution_detections/cell_value_context'; import { StatefulEventsViewer } from '../../../../common/components/events_viewer'; import { defaultRowRenderers } from '../../../../timelines/components/timeline/body/renderers'; import * as i18n from './translations'; @@ -142,7 +143,10 @@ const PreviewHistogramComponent = ({ }, [config, indexPattern, previewId]); return ( - <> + @@ -199,7 +203,7 @@ const PreviewHistogramComponent = ({ bulkActions={false} /> - + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_table_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_table_cell_renderer.tsx index 4db7846ef9ded..0041e726572ba 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_table_cell_renderer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_table_cell_renderer.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; import { TableId } from '@kbn/securitysolution-data-table'; import type { LegacyField } from '@kbn/alerting-types'; @@ -13,6 +13,8 @@ import type { CellValueElementProps } from '../../../../../common/types'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { CellValue } from '../../../../detections/configurations/security_solution_detections'; +const emptyUserProfiles = { profiles: [], isLoading: false }; + export const PreviewRenderCellValue: React.FC< EuiDataGridCellValueElementProps & CellValueElementProps > = ({ @@ -28,11 +30,12 @@ export const PreviewRenderCellValue: React.FC< rowRenderers, truncate, }) => { + const legacyAlert = useMemo(() => (data ?? []) as LegacyField[], [data]); return ( ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 7338bcded1a41..81792a0771322 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -73,6 +73,7 @@ import { AdditionalToolbarControls } from './additional_toolbar_controls'; import { useFetchUserProfilesFromAlerts } from '../../configurations/security_solution_detections/fetch_page_context'; import { useCellActionsOptions } from '../../hooks/trigger_actions_alert_table/use_cell_actions'; import { useAlertsTableFieldsBrowserOptions } from '../../hooks/trigger_actions_alert_table/use_trigger_actions_browser_fields_options'; +import { AlertTableCellContextProvider } from '../../configurations/security_solution_detections/cell_value_context'; const { updateIsLoading, updateTotalCount } = dataTableActions; @@ -172,9 +173,10 @@ const DetectionEngineAlertsTableComponent: FC - - ref={alertsTableRef} - // Stores separate configuration based on the view of the table - id={id ?? `detection-engine-alert-table-${tableType}-${tableView}`} - ruleTypeIds={SECURITY_SOLUTION_RULE_TYPE_IDS} - consumers={ALERT_TABLE_CONSUMERS} - query={finalBoolQuery} - initialSort={initialSort} - casesConfiguration={casesConfiguration} - gridStyle={gridStyle} - shouldHighlightRow={shouldHighlightRow} - rowHeightsOptions={rowHeightsOptions} - columns={finalColumns} - browserFields={finalBrowserFields} - onUpdate={onUpdate} - additionalContext={additionalContext} - height={alertTableHeight} - initialPageSize={50} - runtimeMappings={sourcererDataView?.runtimeFieldMap as RunTimeMappings} - toolbarVisibility={toolbarVisibility} - renderCellValue={CellValue} - renderActionsCell={ActionsCell} - renderAdditionalToolbarControls={ - tableType !== TableId.alertsOnCasePage ? AdditionalToolbarControls : undefined - } - actionsColumnWidth={leadingControlColumn.width} - getBulkActions={getBulkActions} - fieldsBrowserOptions={ - tableType === TableId.alertsOnAlertsPage || - tableType === TableId.alertsOnRuleDetailsPage - ? fieldsBrowserOptions - : undefined - } - cellActionsOptions={cellActionsOptions} - showInspectButton - services={services} - {...tablePropsOverrides} - /> + + + ref={alertsTableRef} + // Stores separate configuration based on the view of the table + id={id ?? `detection-engine-alert-table-${tableType}-${tableView}`} + ruleTypeIds={SECURITY_SOLUTION_RULE_TYPE_IDS} + consumers={ALERT_TABLE_CONSUMERS} + query={finalBoolQuery} + initialSort={initialSort} + casesConfiguration={casesConfiguration} + gridStyle={gridStyle} + shouldHighlightRow={shouldHighlightRow} + rowHeightsOptions={rowHeightsOptions} + columns={finalColumns} + browserFields={finalBrowserFields} + onUpdate={onUpdate} + additionalContext={additionalContext} + height={alertTableHeight} + initialPageSize={50} + runtimeMappings={sourcererDataView?.runtimeFieldMap as RunTimeMappings} + toolbarVisibility={toolbarVisibility} + renderCellValue={CellValue} + renderActionsCell={ActionsCell} + renderAdditionalToolbarControls={ + tableType !== TableId.alertsOnCasePage ? AdditionalToolbarControls : undefined + } + actionsColumnWidth={leadingControlColumn.width} + getBulkActions={getBulkActions} + fieldsBrowserOptions={ + tableType === TableId.alertsOnAlertsPage || + tableType === TableId.alertsOnRuleDetailsPage + ? fieldsBrowserOptions + : undefined + } + cellActionsOptions={cellActionsOptions} + showInspectButton + services={services} + {...tablePropsOverrides} + /> + diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/cell_value_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/cell_value_context.tsx new file mode 100644 index 0000000000000..712cda9ca585a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/cell_value_context.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useMemo } from 'react'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; +import { tableDefaults, dataTableSelectors } from '@kbn/securitysolution-data-table'; +import type { BrowserFields } from '@kbn/timelines-plugin/common'; +import type { SourcererScopeName } from '../../../sourcerer/store/model'; +import { useLicense } from '../../../common/hooks/use_license'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { useSourcererDataView } from '../../../sourcerer/containers'; +import { VIEW_SELECTION } from '../../../../common/constants'; +import { getAllFieldsByName } from '../../../common/containers/source'; +import { eventRenderedViewColumns, getColumns } from './columns'; +import type { AlertColumnHeaders } from './columns'; + +interface AlertTableCellContextProps { + browserFields: BrowserFields; + browserFieldsByName: Record>; + columnHeaders: AlertColumnHeaders; +} + +export const AlertTableCellContext = createContext(null); + +export const AlertTableCellContextProvider = ({ + tableId = '', + sourcererScope, + children, +}: { + tableId?: string; + sourcererScope: SourcererScopeName; + children: React.ReactNode; +}) => { + const { browserFields } = useSourcererDataView(sourcererScope); + const browserFieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]); + const license = useLicense(); + const gridColumns = useMemo(() => { + return getColumns(license); + }, [license]); + const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []); + const viewMode = + useDeepEqualSelector((state) => (getTable(state, tableId ?? '') ?? tableDefaults).viewMode) ?? + tableDefaults.viewMode; + const columnHeaders = useMemo(() => { + return viewMode === VIEW_SELECTION.gridView ? gridColumns : eventRenderedViewColumns; + }, [gridColumns, viewMode]); + + const cellValueContext = useMemo( + () => ({ + browserFields, + browserFieldsByName, + columnHeaders, + }), + [browserFields, browserFieldsByName, columnHeaders] + ); + + return ( + + {children} + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts index 14a5f31b63685..907d18ae81152 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts @@ -137,11 +137,10 @@ const getBaseColumns = ( * columns implements a subset of `EuiDataGrid`'s `EuiDataGridColumn` interface, * plus additional TGrid column properties */ -export const getColumns = ( - license?: LicenseService -): Array< +export type AlertColumnHeaders = Array< Pick & ColumnHeaderOptions -> => [ +>; +export const getColumns = (license?: LicenseService): AlertColumnHeaders => [ { columnHeaderType: defaultColumnHeaderType, id: '@timestamp', diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx index 900c2b0111230..6edad541d10c7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { mount } from 'enzyme'; +import { render } from '@testing-library/react'; import { cloneDeep } from 'lodash/fp'; import type { ComponentProps } from 'react'; import React from 'react'; @@ -16,9 +16,10 @@ import { DragDropContextWrapper } from '../../../common/components/drag_and_drop import { defaultHeaders, mockTimelineData, TestProviders } from '../../../common/mock'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import type { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; -import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; +import type { RenderCellValueProps } from './render_cell_value'; import { CellValue } from './render_cell_value'; import { SourcererScopeName } from '../../../sourcerer/store/model'; +import { AlertTableCellContextProvider } from './cell_value_context'; jest.mock('../../../common/lib/kibana'); jest.mock('../../../sourcerer/containers', () => ({ @@ -41,12 +42,12 @@ describe('RenderCellValue', () => { let data: TimelineNonEcsData[]; let header: ColumnHeaderOptions; - let props: ComponentProps; + let defaultProps: RenderCellValueProps; beforeEach(() => { data = cloneDeep(mockTimelineData[0].data); header = cloneDeep(defaultHeaders[0]); - props = { + defaultProps = { columnId, legacyAlert: data, eventId, @@ -68,37 +69,54 @@ describe('RenderCellValue', () => { } as unknown as ComponentProps; }); - test('it forwards the `CellValueElementProps` to the `DefaultCellRenderer`', () => { - const wrapper = mount( + const RenderCellValueComponent = (props: RenderCellValueProps) => { + return ( - + + + ); + }; - const { legacyAlert, ...defaultCellRendererProps } = props; + it('should throw an error if not wrapped by the AlertTableCellContextProvider', () => { + const renderWithError = () => + render( + + + + + + ); - expect(wrapper.find(DefaultCellRenderer).props()).toEqual({ - ...defaultCellRendererProps, - data: legacyAlert, - scopeId: SourcererScopeName.default, - }); + expect(renderWithError).toThrow( + 'render_cell_value.tsx: CellValue must be used within AlertTableCellContextProvider' + ); }); - test('it renders a GuidedOnboardingTourStep', () => { - const wrapper = mount( - - - - - - ); + it('should fully render the cell value', () => { + const { getByText } = render(); + + expect(getByText('Nov 5, 2018 @ 19:03:25.937')).toBeInTheDocument(); + }); + + it('should render the guided onboarding step', () => { + const { getByTestId } = render(); - expect(wrapper.find('[data-test-subj="GuidedOnboardingTourStep"]').exists()).toEqual(true); + expect(getByTestId('GuidedOnboardingTourStep')).toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx index 96d36d2f5883c..8be83b6577b18 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx @@ -5,13 +5,11 @@ * 2.0. */ -import React, { useMemo, memo, type ComponentProps } from 'react'; +import React, { useMemo, memo, type ComponentProps, useContext } from 'react'; import { EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { find, getOr } from 'lodash/fp'; import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; -import { tableDefaults, dataTableSelectors } from '@kbn/securitysolution-data-table'; -import { useLicense } from '../../../common/hooks/use_license'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { useKibana } from '../../../common/lib/kibana'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import { GuidedOnboardingTourStep } from '../../../common/components/guided_onboarding_tour/tour_step'; import { isDetectionsAlertsTable } from '../../../common/components/top_n/helpers'; @@ -20,15 +18,12 @@ import { SecurityStepId, } from '../../../common/components/guided_onboarding_tour/tour_config'; import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants'; -import { useSourcererDataView } from '../../../sourcerer/containers'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { SUPPRESSED_ALERT_TOOLTIP } from './translations'; -import { VIEW_SELECTION } from '../../../../common/constants'; -import { getAllFieldsByName } from '../../../common/containers/source'; -import { eventRenderedViewColumns, getColumns } from './columns'; import type { GetSecurityAlertsTableProp } from '../../components/alerts_table/types'; import type { CellValueElementProps, ColumnHeaderOptions } from '../../../../common/types'; +import { AlertTableCellContext } from './cell_value_context'; /** * This implementation of `EuiDataGrid`'s `renderCellValue` @@ -36,7 +31,7 @@ import type { CellValueElementProps, ColumnHeaderOptions } from '../../../../com * from the TGrid */ -type RenderCellValueProps = Pick< +export type RenderCellValueProps = Pick< ComponentProps>, | 'columnId' | 'rowIndex' @@ -76,6 +71,7 @@ export const CellValue = memo(function RenderCellValue({ truncate, userProfiles, }: RenderCellValueProps) { + const { notifications } = useKibana().services; const isTourAnchor = useMemo( () => columnId === SIGNAL_RULE_NAME_FIELD_NAME && @@ -84,22 +80,21 @@ export const CellValue = memo(function RenderCellValue({ !isDetails, [columnId, isDetails, rowIndex, tableType] ); - const { browserFields } = useSourcererDataView(sourcererScope); - const browserFieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]); - const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []); - const license = useLicense(); - const viewMode = - useDeepEqualSelector((state) => (getTable(state, tableId ?? '') ?? tableDefaults).viewMode) ?? - tableDefaults.viewMode; + const cellValueContext = useContext(AlertTableCellContext); - const gridColumns = useMemo(() => { - return getColumns(license); - }, [license]); + if (!cellValueContext) { + const contextMissingError = new Error( + 'render_cell_value.tsx: CellValue must be used within AlertTableCellContextProvider' + ); - const columnHeaders = useMemo(() => { - return viewMode === VIEW_SELECTION.gridView ? gridColumns : eventRenderedViewColumns; - }, [gridColumns, viewMode]); + notifications.toasts.addError(contextMissingError, { + title: 'AlertTableCellContextProvider is missing', + toastMessage: 'CellValue must be used within AlertTableCellContextProvider', + }); + throw new Error(contextMissingError.message); + } + const { browserFields, browserFieldsByName, columnHeaders } = cellValueContext; /** * There is difference between how `triggers actions` fetched data v/s * how security solution fetches data via timelineSearchStrategy @@ -134,11 +129,21 @@ export const CellValue = memo(function RenderCellValue({ return ecsSuppressionCount ? parseInt(ecsSuppressionCount, 10) : dataSuppressionCount; }, [ecsAlert, legacyAlert]); - const Renderer = useMemo(() => { - const myHeader = - header ?? ({ id: columnId, ...browserFieldsByName[columnId] } as ColumnHeaderOptions); - const colHeader = columnHeaders.find((col) => col.id === columnId); - const localLinkValues = getOr([], colHeader?.linkField ?? '', ecsAlert); + const myHeader = useMemo( + () => header ?? ({ id: columnId, ...browserFieldsByName[columnId] } as ColumnHeaderOptions), + [browserFieldsByName, columnId, header] + ); + + const colHeader = useMemo( + () => columnHeaders.find((col) => col.id === columnId), + [columnHeaders, columnId] + ); + const localLinkValues = useMemo( + () => getOr([], colHeader?.linkField ?? '', ecsAlert), + [colHeader?.linkField, ecsAlert] + ); + + const CellRenderer = useMemo(() => { return ( ); }, [ - header, - columnId, - browserFieldsByName, - columnHeaders, - ecsAlert, isTourAnchor, browserFields, + columnId, finalData, + ecsAlert, eventId, + myHeader, isDetails, - isExpandable, isExpanded, linkValues, + localLinkValues, rowIndex, colIndex, rowRenderers, @@ -198,9 +201,9 @@ export const CellValue = memo(function RenderCellValue({ - {Renderer} + {CellRenderer} ) : ( - <>{Renderer} + <>{CellRenderer} ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/migrations/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/migrations/index.ts index 498d9c954c206..a68a2591fe263 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/migrations/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/migrations/index.ts @@ -13,6 +13,7 @@ import { assetCrticalityCopyTimestampToEventIngested } from './asset_criticality import { riskScoreCopyTimestampToEventIngested } from './risk_score_copy_timestamp_to_event_ingested'; import { updateAssetCriticalityMappings } from '../asset_criticality/migrations/update_asset_criticality_mappings'; import { updateRiskScoreMappings } from '../risk_engine/migrations/update_risk_score_mappings'; +import { renameRiskScoreComponentTemplate } from '../risk_engine/migrations/rename_risk_score_component_templates'; export interface EntityAnalyticsMigrationsParams { taskManager?: TaskManagerSetupContract; @@ -43,6 +44,7 @@ export const scheduleEntityAnalyticsMigration = async (params: EntityAnalyticsMi await updateAssetCriticalityMappings({ ...params, logger: scopedLogger }); await scheduleAssetCriticalityEcsCompliancyMigration({ ...params, logger: scopedLogger }); + await renameRiskScoreComponentTemplate({ ...params, logger: scopedLogger }); await updateRiskScoreMappings({ ...params, logger: scopedLogger }); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/migrations/rename_risk_score_component_templates.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/migrations/rename_risk_score_component_templates.test.ts new file mode 100644 index 0000000000000..799722904b9eb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/migrations/rename_risk_score_component_templates.test.ts @@ -0,0 +1,232 @@ +/* + * 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 { renameRiskScoreComponentTemplate } from './rename_risk_score_component_templates'; +import { + loggingSystemMock, + savedObjectsClientMock as mockSavedObjectsClient, + elasticsearchServiceMock, +} from '@kbn/core/server/mocks'; +import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; + +const mockCreateComponentTemplate = jest.fn(); +const mockCreateIndexTemplate = jest.fn(); + +jest.mock('../../risk_score/risk_score_data_client', () => ({ + RiskScoreDataClient: jest.fn().mockImplementation(() => ({ + createOrUpdateRiskScoreComponentTemplate: () => mockCreateComponentTemplate(), + createOrUpdateRiskScoreIndexTemplate: () => mockCreateIndexTemplate(), + })), +})); + +jest.mock('../../risk_score/tasks/helpers', () => ({ + buildScopedInternalSavedObjectsClientUnsafe: () => mockSavedObjectsClient.create(), +})); + +const buildSavedObjectResponse = (namespaces = ['default']) => ({ + page: 1, + per_page: 20, + total: namespaces.length, + saved_objects: namespaces.map((namespace) => ({ + namespaces: [namespace], + attributes: {}, + id: 'id', + type: 'type', + references: [], + score: 1, + })), +}); + +describe('renameRiskScoreComponentTemplate', () => { + const mockGetStartServices = jest.fn(); + const mockAuditLogger = auditLoggerMock.create(); + const mockLogger = loggingSystemMock.createLogger(); + const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const mockSoClient = mockSavedObjectsClient.create(); + + beforeEach(() => { + jest.clearAllMocks(); + mockCreateComponentTemplate.mockReset(); + mockCreateIndexTemplate.mockReset(); + mockGetStartServices.mockResolvedValue([ + { + savedObjects: { + createInternalRepository: jest.fn().mockReturnValue(mockSoClient), + }, + elasticsearch: { + client: { + asInternalUser: mockEsClient, + }, + }, + }, + ]); + }); + + it('should not proceed if old component template does not exist', async () => { + mockEsClient.cluster.existsComponentTemplate.mockResolvedValue(false); + + await renameRiskScoreComponentTemplate({ + auditLogger: mockAuditLogger, + logger: mockLogger, + getStartServices: mockGetStartServices, + kibanaVersion: '8.0.0', + }); + + expect(mockEsClient.cluster.existsComponentTemplate).toHaveBeenCalledWith({ + name: '.risk-score-mappings', + }); + expect(mockEsClient.cluster.deleteComponentTemplate).not.toHaveBeenCalled(); + }); + + it('should proceed with migration if old component template exists', async () => { + mockEsClient.cluster.existsComponentTemplate.mockResolvedValue(true); + mockSoClient.find.mockResolvedValue(buildSavedObjectResponse(['default'])); + + await renameRiskScoreComponentTemplate({ + auditLogger: mockAuditLogger, + logger: mockLogger, + getStartServices: mockGetStartServices, + kibanaVersion: '8.0.0', + }); + + expect(mockEsClient.cluster.existsComponentTemplate).toHaveBeenCalledWith({ + name: '.risk-score-mappings', + }); + expect(mockEsClient.cluster.deleteComponentTemplate).toHaveBeenCalledWith( + { name: '.risk-score-mappings' }, + { ignore: [404] } + ); + }); + + it('should log an error if a saved object has no namespace', async () => { + const savedObj = buildSavedObjectResponse([]); + mockEsClient.cluster.existsComponentTemplate.mockResolvedValue(true); + mockSoClient.find.mockResolvedValue({ + ...savedObj, + saved_objects: [ + { + ...savedObj.saved_objects[0], + namespaces: [], + }, + ], + }); + + await renameRiskScoreComponentTemplate({ + auditLogger: mockAuditLogger, + logger: mockLogger, + getStartServices: mockGetStartServices, + kibanaVersion: '8.0.0', + }); + + expect(mockLogger.error).toHaveBeenCalledWith( + 'Unexpected saved object. Risk Score saved objects must have a namespace' + ); + }); + + it('should throw an error if any promise is rejected', async () => { + mockEsClient.cluster.existsComponentTemplate.mockResolvedValue(true); + mockSoClient.find.mockResolvedValue(buildSavedObjectResponse(['default'])); + mockCreateComponentTemplate.mockRejectedValue(new Error('Test error')); + + await expect( + renameRiskScoreComponentTemplate({ + auditLogger: mockAuditLogger, + logger: mockLogger, + getStartServices: mockGetStartServices, + kibanaVersion: '8.0.0', + }) + ).rejects.toThrow('Risk Score component template migration failed with errors: \nTest error'); + expect(mockEsClient.cluster.deleteComponentTemplate).not.toHaveBeenCalled(); + }); + + it('should throw an error with concatenated error messages when more than one error happens', async () => { + mockEsClient.cluster.existsComponentTemplate.mockResolvedValue(true); + mockSoClient.find.mockResolvedValue(buildSavedObjectResponse(['space-1', 'space-2'])); + + mockCreateComponentTemplate + .mockRejectedValueOnce(new Error('Test error 1')) + .mockRejectedValueOnce(new Error('Test error 2')); + + await expect( + renameRiskScoreComponentTemplate({ + auditLogger: mockAuditLogger, + logger: mockLogger, + getStartServices: mockGetStartServices, + kibanaVersion: '8.0.0', + }) + ).rejects.toThrow( + 'Risk Score component template migration failed with errors: \nTest error 1\nTest error 2' + ); + }); + + it('should handle errors when creating/updating index template', async () => { + mockEsClient.cluster.existsComponentTemplate.mockResolvedValue(true); + mockSoClient.find.mockResolvedValue(buildSavedObjectResponse(['default'])); + + mockCreateIndexTemplate.mockRejectedValue(new Error('Index template error')); + + await expect( + renameRiskScoreComponentTemplate({ + auditLogger: mockAuditLogger, + logger: mockLogger, + getStartServices: mockGetStartServices, + kibanaVersion: '8.0.0', + }) + ).rejects.toThrow( + 'Risk Score component template migration failed with errors: \nIndex template error' + ); + }); + + it('should log info when migration starts for a namespace', async () => { + mockEsClient.cluster.existsComponentTemplate.mockResolvedValue(true); + mockSoClient.find.mockResolvedValue(buildSavedObjectResponse(['default'])); + + await renameRiskScoreComponentTemplate({ + auditLogger: mockAuditLogger, + logger: mockLogger, + getStartServices: mockGetStartServices, + kibanaVersion: '8.0.0', + }); + + expect(mockLogger.info).toHaveBeenCalledWith( + 'Starting Risk Score component template migration on namespace default' + ); + }); + + it('should log debug when migration completes for a namespace', async () => { + mockEsClient.cluster.existsComponentTemplate.mockResolvedValue(true); + mockSoClient.find.mockResolvedValue(buildSavedObjectResponse(['default'])); + + await renameRiskScoreComponentTemplate({ + auditLogger: mockAuditLogger, + logger: mockLogger, + getStartServices: mockGetStartServices, + kibanaVersion: '8.0.0', + }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + 'Risk score component template migration ran on namespace default' + ); + }); + + it('should delete the old component template if all migrations succeed', async () => { + mockEsClient.cluster.existsComponentTemplate.mockResolvedValue(true); + mockSoClient.find.mockResolvedValue(buildSavedObjectResponse(['default'])); + + await renameRiskScoreComponentTemplate({ + auditLogger: mockAuditLogger, + logger: mockLogger, + getStartServices: mockGetStartServices, + kibanaVersion: '8.0.0', + }); + + expect(mockEsClient.cluster.deleteComponentTemplate).toHaveBeenCalledWith( + { name: '.risk-score-mappings' }, + { ignore: [404] } + ); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/migrations/rename_risk_score_component_templates.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/migrations/rename_risk_score_component_templates.ts new file mode 100644 index 0000000000000..cfcbaebfd799a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/migrations/rename_risk_score_component_templates.ts @@ -0,0 +1,96 @@ +/* + * 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 { EntityAnalyticsMigrationsParams } from '../../migrations'; +import { RiskScoreDataClient } from '../../risk_score/risk_score_data_client'; +import type { RiskEngineConfiguration } from '../../types'; +import { riskEngineConfigurationTypeName } from '../saved_object'; +import { buildScopedInternalSavedObjectsClientUnsafe } from '../../risk_score/tasks/helpers'; +import { mappingComponentName } from '../../risk_score/configurations'; + +export const MAX_PER_PAGE = 10_000; + +/** + * This migration renames the Risk Score component templates to include the namespace in the name. Before 8.18 all spaces used the `.risk-score-mappings` component template, we now use `.risk-score-mappings-`. + * + * The migration creates the new component template and updates the index template for each space, then finally deletes the old component template. + */ +export const renameRiskScoreComponentTemplate = async ({ + auditLogger, + logger, + getStartServices, + kibanaVersion, +}: EntityAnalyticsMigrationsParams) => { + const [coreStart] = await getStartServices(); + const soClientKibanaUser = coreStart.savedObjects.createInternalRepository(); + const esClient = coreStart.elasticsearch.client.asInternalUser; + + // Check if the legacy component templates (without the namespace in the name) exists + const oldComponentTemplateExists = await esClient.cluster.existsComponentTemplate({ + name: mappingComponentName, + }); + + if (!oldComponentTemplateExists) { + return; + } + + // Get all installed Risk Engine Configurations + const savedObjectsResponse = await soClientKibanaUser.find({ + type: riskEngineConfigurationTypeName, + perPage: MAX_PER_PAGE, + namespaces: ['*'], + }); + + const settledPromises = await Promise.allSettled( + savedObjectsResponse.saved_objects.map(async (savedObject) => { + const namespace = savedObject.namespaces?.[0]; // We need to create one component template per space + + if (!namespace) { + logger.error('Unexpected saved object. Risk Score saved objects must have a namespace'); + return; + } + + logger.info(`Starting Risk Score component template migration on namespace ${namespace}`); + + const soClient = buildScopedInternalSavedObjectsClientUnsafe({ coreStart, namespace }); + + const riskScoreDataClient = new RiskScoreDataClient({ + logger, + kibanaVersion, + esClient, + namespace, + soClient, + auditLogger, + }); + + await riskScoreDataClient.createOrUpdateRiskScoreComponentTemplate(); + await riskScoreDataClient.createOrUpdateRiskScoreIndexTemplate(); + + logger.debug(`Risk score component template migration ran on namespace ${namespace}`); + }) + ); + + const rejectedPromises = settledPromises.filter( + (promise) => promise.status === 'rejected' + ) as PromiseRejectedResult[]; + + // Migration successfully ran on all spaces + if (rejectedPromises.length === 0) { + // Delete the legacy component template without the namespace in the name + await esClient.cluster.deleteComponentTemplate( + { + name: mappingComponentName, + }, + { ignore: [404] } + ); + } else { + const errorMessages = rejectedPromises.map((promise) => promise.reason?.message).join('\n'); + throw new Error( + `Risk Score component template migration failed with errors: \n${errorMessages}` + ); + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/migrations/update_risk_score_mappings.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/migrations/update_risk_score_mappings.ts index d671dee80799e..60d6dd311d30d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/migrations/update_risk_score_mappings.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/migrations/update_risk_score_mappings.ts @@ -68,7 +68,7 @@ export const updateRiskScoreMappings = async ({ }); await riskScoreDataClient.createOrUpdateRiskScoreLatestIndex(); - await riskScoreDataClient.createOrUpdateRiskScoreIndexTemplate(); + await riskScoreDataClient.createOrUpdateRiskScoreComponentTemplate(); await riskScoreDataClient.updateRiskScoreTimeSeriesIndexMappings(); await riskEngineDataClient.updateConfiguration({ _meta: { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts index cd965886d5d63..6cd942e288955 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts @@ -111,7 +111,7 @@ export class RiskScoreDataClient { }); }; - public createOrUpdateRiskScoreIndexTemplate = async () => + public createOrUpdateRiskScoreComponentTemplate = async () => createOrUpdateComponentTemplate({ logger: this.options.logger, esClient: this.options.esClient, @@ -128,6 +128,40 @@ export class RiskScoreDataClient { totalFieldsLimit, }); + public createOrUpdateRiskScoreIndexTemplate = async () => { + const indexPatterns = getIndexPatternDataStream(this.options.namespace); + const indexMetadata: Metadata = { + kibana: { + version: this.options.kibanaVersion, + }, + managed: true, + namespace: this.options.namespace, + }; + + return createOrUpdateIndexTemplate({ + logger: this.options.logger, + esClient: this.options.esClient, + template: { + name: indexPatterns.template, + data_stream: { hidden: true }, + index_patterns: [indexPatterns.alias], + composed_of: [nameSpaceAwareMappingsComponentName(this.options.namespace)], + template: { + lifecycle: {}, + settings: { + 'index.mapping.total_fields.limit': totalFieldsLimit, + 'index.default_pipeline': getIngestPipelineName(this.options.namespace), + }, + mappings: { + dynamic: false, + _meta: indexMetadata, + }, + }, + _meta: indexMetadata, + }, + }); + }; + public updateRiskScoreTimeSeriesIndexMappings = async () => updateUnderlyingMapping({ esClient: this.options.esClient, @@ -142,59 +176,11 @@ export class RiskScoreDataClient { try { await createEventIngestedFromTimestamp(esClient, namespace); - const indexPatterns = getIndexPatternDataStream(namespace); - - const indexMetadata: Metadata = { - kibana: { - version: this.options.kibanaVersion, - }, - managed: true, - namespace, - }; - - // Check if there are any existing component templates with the namespace in the name - const oldComponentTemplateExists = await esClient.cluster.existsComponentTemplate({ - name: mappingComponentName, - }); - if (oldComponentTemplateExists) { - await this.updateComponentTemplateNameWithNamespace(namespace); - } + await this.createOrUpdateRiskScoreComponentTemplate(); - // Update the new component template with the required data await this.createOrUpdateRiskScoreIndexTemplate(); - // Reference the new component template in the index template - await createOrUpdateIndexTemplate({ - logger: this.options.logger, - esClient, - template: { - name: indexPatterns.template, - data_stream: { hidden: true }, - index_patterns: [indexPatterns.alias], - composed_of: [nameSpaceAwareMappingsComponentName(namespace)], - template: { - lifecycle: {}, - settings: { - 'index.mapping.total_fields.limit': totalFieldsLimit, - 'index.default_pipeline': getIngestPipelineName(namespace), - }, - mappings: { - dynamic: false, - _meta: indexMetadata, - }, - }, - _meta: indexMetadata, - }, - }); - - // Delete the component template without the namespace in the name - await esClient.cluster.deleteComponentTemplate( - { - name: mappingComponentName, - }, - { ignore: [404] } - ); - + const indexPatterns = getIndexPatternDataStream(namespace); await createDataStream({ logger: this.options.logger, esClient, @@ -331,23 +317,6 @@ export class RiskScoreDataClient { ); } - private async updateComponentTemplateNameWithNamespace(namespace: string): Promise { - const esClient = this.options.esClient; - const oldComponentTemplateResponse = await esClient.cluster.getComponentTemplate( - { - name: mappingComponentName, - }, - { ignore: [404] } - ); - const oldComponentTemplate = oldComponentTemplateResponse?.component_templates[0]; - const newComponentTemplateName = nameSpaceAwareMappingsComponentName(namespace); - await esClient.cluster.putComponentTemplate({ - name: newComponentTemplateName, - // @ts-expect-error elasticsearch@9.0.0 https://github.com/elastic/elasticsearch-js/issues/2584 - body: oldComponentTemplate.component_template, - }); - } - public copyTimestampToEventIngestedForRiskScore = (abortSignal?: AbortSignal) => { return this.options.esClient.updateByQuery( { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/agent.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/agent.spec.ts index 8d96b00867e80..22953fced1fee 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/agent.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/agent.spec.ts @@ -58,7 +58,10 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon expect(response.status).to.be(200); - expect(response.body).to.eql({ agentName: 'nodejs', runtimeName: 'node' }); + expect(response.body).to.eql({ + agentName: 'nodejs', + runtimeName: 'node', + }); }); }); }); diff --git a/x-pack/test/fleet_api_integration/apis/fleet_proxies/crud.ts b/x-pack/test/fleet_api_integration/apis/fleet_proxies/crud.ts index b3e3454dafe7a..de0304ad9e024 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_proxies/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_proxies/crud.ts @@ -33,8 +33,7 @@ export default function (providerContext: FtrProviderContext) { return policyDocRes.hits.hits[0]?._source; } - // FLAKY: https://github.com/elastic/kibana/issues/207024 - describe.skip('fleet_proxies_crud', function () { + describe('fleet_proxies_crud', function () { const existingId = 'test-default-123'; const fleetServerHostId = 'test-fleetserver-123'; const policyId = 'test-policy-123'; @@ -180,7 +179,7 @@ export default function (providerContext: FtrProviderContext) { ); }, { - retryCount: 10, + retryCount: 20, timeout: 30_1000, } ); @@ -217,7 +216,7 @@ export default function (providerContext: FtrProviderContext) { expect(fleetPolicyAfter?.data?.agent.download.proxy_url).to.be(undefined); }, { - retryCount: 10, + retryCount: 20, timeout: 30_1000, } ); diff --git a/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts b/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts index 4ff6da617bbd3..8a6f5984890e6 100644 --- a/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts +++ b/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const testSubjects = getService('testSubjects'); const elasticChart = getService('elasticChart'); + const toastsService = getService('toasts'); const createNewLens = async () => { await visualize.navigateToNewVisualization(); @@ -229,6 +230,53 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await timeToVisualize.resetNewDashboard(); }); + it('should allow adding a by reference annotation', async () => { + const ANNOTATION_GROUP_TITLE = 'My by reference annotation group'; + await loadExistingLens(); + await lens.save('xyVisChart Copy 2', true, false, false, 'new'); + + await dashboard.waitForRenderComplete(); + await elasticChart.setNewChartUiDebugFlag(true); + + await dashboardPanelActions.clickInlineEdit(); + + log.debug('Adds by reference annotation'); + + await lens.createLayer('annotations'); + + await lens.performLayerAction('lnsXY_annotationLayer_saveToLibrary', 1); + + await visualize.setSaveModalValues(ANNOTATION_GROUP_TITLE, { + description: 'my description', + }); + + await testSubjects.click('confirmSaveSavedObjectButton'); + + const toastContents = await toastsService.getContentByIndex(1); + + expect(toastContents).to.be( + `Saved "${ANNOTATION_GROUP_TITLE}"\nView or manage in the annotation library.` + ); + + // now close + await testSubjects.click('applyFlyoutButton'); + + log.debug('Edit the by reference annotation'); + // and try to edit again the by reference annotation layer event + await dashboardPanelActions.clickInlineEdit(); + + expect((await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length).to.eql(2); + expect( + await ( + await testSubjects.find('lnsXY_xAnnotationsPanel > lns-dimensionTrigger') + ).getVisibleText() + ).to.eql('Event'); + + await dashboard.waitForRenderComplete(); + + await timeToVisualize.resetNewDashboard(); + }); + it('should allow adding a reference line', async () => { await loadExistingLens(); await lens.save('xyVisChart Copy', true, false, false, 'new'); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/enable_risk_score_redirect.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/enable_risk_score_redirect.cy.ts deleted file mode 100644 index 588263aa42c6b..0000000000000 --- a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/enable_risk_score_redirect.cy.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ENABLE_RISK_SCORE_BUTTON } from '../../../screens/entity_analytics'; - -import { login } from '../../../tasks/login'; -import { visit } from '../../../tasks/navigation'; -import { clickEnableRiskScore } from '../../../tasks/risk_scores'; - -import { ENTITY_ANALYTICS_URL } from '../../../urls/navigation'; -import { PAGE_TITLE } from '../../../screens/entity_analytics_management'; - -// Failing: See https://github.com/elastic/kibana/issues/206580 -describe.skip( - 'Enable risk scores from dashboard', - { - tags: ['@ess', '@serverless'], - env: { - ftrConfig: { - kbnServerArgs: [ - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['entityStoreDisabled'])}`, - ], - }, - }, - }, - () => { - beforeEach(() => { - login(); - visit(ENTITY_ANALYTICS_URL); - }); - - it('risk enable button should redirect to entity management page', () => { - cy.get(ENABLE_RISK_SCORE_BUTTON).should('exist'); - - clickEnableRiskScore(); - - cy.get(PAGE_TITLE).should('have.text', 'Entity Risk Score'); - }); - } -); diff --git a/yarn.lock b/yarn.lock index a9e3f5e3d6726..911de405f1422 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2283,21 +2283,21 @@ resolved "https://registry.yarnpkg.com/@elastic/eui-theme-borealis/-/eui-theme-borealis-0.0.11.tgz#edad01998f2de79b5d6b401f8951fbe10353e2d6" integrity sha512-M8tDj6zDkbM/K4G/MVol64CUO2e+d9wAlZWZ7odcZHBTAbxD35I3HQy0l0+z6EUzE02+2jUmPCK8IykpKLjPdg== -"@elastic/eui-theme-common@0.0.10": - version "0.0.10" - resolved "https://registry.yarnpkg.com/@elastic/eui-theme-common/-/eui-theme-common-0.0.10.tgz#253da1337abae656ab93917bb3ed7e6f48e5f9a1" - integrity sha512-MruijLreN1VpyQgUK2pXcdFIbpYpjc3ZaZ+f0r1Aa0KON935wIf7VAb861tYPmAJxaf+4X4kyoqS0F+8rO9tbg== +"@elastic/eui-theme-common@0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@elastic/eui-theme-common/-/eui-theme-common-0.0.11.tgz#950d06a652a25b09c46cec8f16a449223ada45a3" + integrity sha512-DV8qX45R/01SWdCoINWbFwJgMdWHYuq7ZmdY/J3pE1QNKIGv52SdOpiW+XwhT8S3dOv+XFidoywVT9pAjcMmyA== dependencies: "@types/lodash" "^4.14.202" chroma-js "^2.4.2" lodash "^4.17.21" -"@elastic/eui@99.4.0-borealis.0": - version "99.4.0-borealis.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-99.4.0-borealis.0.tgz#8637698371fa3c6827da4f7cb689d3cd39de3841" - integrity sha512-ws2OVe67WrO9CBkembcqBXFh3Lo10HRvk19MZZL22RoZ3dyGlNr0WTxvZpLPEykycOaOCnf3hAo9p8V16wPWvw== +"@elastic/eui@100.0.0": + version "100.0.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-100.0.0.tgz#7da6beb5c9b2592a8784835f1450388dcda5f51c" + integrity sha512-8A1pYmrw3N/N8ny6tDRYYSmetOBgz3j7KkARr+LWskHMLU7l+nduK+1dVn19qR15C85ufKOyZxAoZeYpxsppsQ== dependencies: - "@elastic/eui-theme-common" "0.0.10" + "@elastic/eui-theme-common" "0.0.11" "@elastic/prismjs-esql" "^1.0.0" "@hello-pangea/dnd" "^16.6.0" "@types/lodash" "^4.14.202"