From ef5b327084bf6132644e4bafcd6618c525be8b74 Mon Sep 17 00:00:00 2001 From: christineweng Date: Fri, 10 Apr 2026 12:14:34 -0500 Subject: [PATCH 1/3] persistable migration part 2 ML and AIOPS --- .../aiops_change_point_detection/constants.ts | 2 - .../aiops_log_pattern_analysis/constants.ts | 2 - .../ml/aiops_log_rate_analysis/constants.ts | 2 - .../plugins/shared/aiops/common/utils.ts | 19 ++ .../cases/change_point_charts_attachment.tsx | 16 +- .../public/cases/log_pattern_attachment.tsx | 15 +- .../cases/log_rate_analysis_attachment.tsx | 9 +- .../aiops/public/cases/register_cases.tsx | 24 ++- .../aiops/public/hooks/use_cases_modal.ts | 24 ++- .../plugins/shared/aiops/server/plugin.ts | 4 +- .../shared/aiops/server/register_cases.ts | 26 ++- .../cases/common/constants/attachments.ts | 25 +++ .../plugins/shared/cases/common/index.ts | 6 + .../client/attachment_framework/types.ts | 1 + .../user_actions/comment/comment.test.tsx | 186 ++++++++---------- .../user_actions/comment/comment.tsx | 41 +--- .../comment/persistable_state.tsx | 48 ----- .../shared/ml/common/constants/cases.ts | 10 - .../shared/ml/common/util/cases_utils.ts | 18 ++ .../contexts/kibana/use_cases_modal.ts | 35 ++-- .../cases/anomaly_charts_attachments.tsx | 15 +- .../cases/anomaly_swim_lane_attachment.tsx | 19 +- .../register_anomaly_charts_attachment.tsx | 8 +- .../register_anomaly_swim_lane_attachment.tsx | 8 +- ...gister_single_metric_viewer_attachment.tsx | 8 +- .../cases/single_metric_viewer_attachment.tsx | 15 +- .../shared/ml/server/lib/register_cases.ts | 32 +-- .../registered_unified_attachment_types.ts | 6 + .../registered_persistable_state_trial.ts | 6 - 29 files changed, 299 insertions(+), 331 deletions(-) create mode 100644 x-pack/platform/plugins/shared/aiops/common/utils.ts delete mode 100644 x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/persistable_state.tsx delete mode 100644 x-pack/platform/plugins/shared/ml/common/constants/cases.ts create mode 100644 x-pack/platform/plugins/shared/ml/common/util/cases_utils.ts diff --git a/x-pack/platform/packages/private/ml/aiops_change_point_detection/constants.ts b/x-pack/platform/packages/private/ml/aiops_change_point_detection/constants.ts index 450275d8cc51a..b225fff546217 100644 --- a/x-pack/platform/packages/private/ml/aiops_change_point_detection/constants.ts +++ b/x-pack/platform/packages/private/ml/aiops_change_point_detection/constants.ts @@ -11,8 +11,6 @@ */ export const CHANGE_POINT_DETECTION_ENABLED = true; -export const CASES_ATTACHMENT_CHANGE_POINT_CHART = 'aiopsChangePointChart'; - // // Do not change constant value - part of public REST APIs // diff --git a/x-pack/platform/packages/shared/ml/aiops_log_pattern_analysis/constants.ts b/x-pack/platform/packages/shared/ml/aiops_log_pattern_analysis/constants.ts index 4c8ffb9b57dfd..13b4e3acbea60 100644 --- a/x-pack/platform/packages/shared/ml/aiops_log_pattern_analysis/constants.ts +++ b/x-pack/platform/packages/shared/ml/aiops_log_pattern_analysis/constants.ts @@ -5,8 +5,6 @@ * 2.0. */ -export const CASES_ATTACHMENT_LOG_PATTERN = 'aiopsPatternAnalysisEmbeddable'; - // // Do not change constant value - part of public REST APIs // diff --git a/x-pack/platform/packages/shared/ml/aiops_log_rate_analysis/constants.ts b/x-pack/platform/packages/shared/ml/aiops_log_rate_analysis/constants.ts index 4351fa302cea6..04835422433f6 100644 --- a/x-pack/platform/packages/shared/ml/aiops_log_rate_analysis/constants.ts +++ b/x-pack/platform/packages/shared/ml/aiops_log_rate_analysis/constants.ts @@ -49,5 +49,3 @@ export const EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE = 'aiops_log_rate_analysis' as co /** */ export const LOG_RATE_ANALYSIS_DATA_VIEW_REF_NAME = 'aiopsLogRateAnalysisEmbeddableDataViewId'; - -export const CASES_ATTACHMENT_LOG_RATE_ANALYSIS = 'aiopsLogRateAnalysisEmbeddable'; diff --git a/x-pack/platform/plugins/shared/aiops/common/utils.ts b/x-pack/platform/plugins/shared/aiops/common/utils.ts new file mode 100644 index 0000000000000..00b2ca61cd45f --- /dev/null +++ b/x-pack/platform/plugins/shared/aiops/common/utils.ts @@ -0,0 +1,19 @@ +/* + * 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 { isPlainObject } from 'lodash'; + +export const casesSchemaValidator = (data: unknown) => { + if (!isPlainObject(data) || data === null) { + throw new Error('Persistable attachment data must be an object'); + } + + const state = (data as Record).state; + if (!isPlainObject(state) || state === null) { + throw new Error('Persistable attachment data must include object "state"'); + } +}; diff --git a/x-pack/platform/plugins/shared/aiops/public/cases/change_point_charts_attachment.tsx b/x-pack/platform/plugins/shared/aiops/public/cases/change_point_charts_attachment.tsx index caa6fb4839c14..5f516c31d9414 100644 --- a/x-pack/platform/plugins/shared/aiops/public/cases/change_point_charts_attachment.tsx +++ b/x-pack/platform/plugins/shared/aiops/public/cases/change_point_charts_attachment.tsx @@ -8,7 +8,7 @@ import { memoize } from 'lodash'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import React from 'react'; -import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; +import type { UnifiedValueAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; import type { TimeRange } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -25,17 +25,15 @@ export const initComponent = memoize( ChangePointDetectionComponent: ChangePointDetectionSharedComponent ) => { return React.memo( - (props: PersistableStateAttachmentViewProps) => { - const { persistableStateAttachmentState } = props; - + (props: UnifiedValueAttachmentViewProps) => { const dataFormatter = fieldFormats.deserialize({ id: FIELD_FORMAT_IDS.DATE, }); - const rawState = persistableStateAttachmentState as unknown as Record; + const rawState = props.data.state as Record; const timeRange = (rawState.time_range ?? rawState.timeRange) as TimeRange; const inputProps = { - ...(persistableStateAttachmentState as unknown as ChangePointDetectionProps), + ...(rawState as unknown as ChangePointDetectionProps), timeRange, }; @@ -60,11 +58,7 @@ export const initComponent = memoize( ); }, - (prevProps, nextProps) => - deepEqual( - prevProps.persistableStateAttachmentState, - nextProps.persistableStateAttachmentState - ) + (prevProps, nextProps) => deepEqual(prevProps.data.state, nextProps.data.state) ); } ); diff --git a/x-pack/platform/plugins/shared/aiops/public/cases/log_pattern_attachment.tsx b/x-pack/platform/plugins/shared/aiops/public/cases/log_pattern_attachment.tsx index 1d08c99197725..2cd25f2546848 100644 --- a/x-pack/platform/plugins/shared/aiops/public/cases/log_pattern_attachment.tsx +++ b/x-pack/platform/plugins/shared/aiops/public/cases/log_pattern_attachment.tsx @@ -8,7 +8,7 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { memoize } from 'lodash'; import React from 'react'; -import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; +import type { UnifiedValueAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; import type { TimeRange } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -22,17 +22,16 @@ import type { export const initComponent = memoize( (fieldFormats: FieldFormatsStart, PatternAnalysisComponent: PatternAnalysisSharedComponent) => { return React.memo( - (props: PersistableStateAttachmentViewProps) => { - const { persistableStateAttachmentState } = props; + (props: UnifiedValueAttachmentViewProps) => { + const rawState = props.data.state as Record; const dataFormatter = fieldFormats.deserialize({ id: FIELD_FORMAT_IDS.DATE, }); - const rawState = persistableStateAttachmentState as unknown as Record; const timeRange = (rawState.time_range ?? rawState.timeRange) as TimeRange; const inputProps = { - ...(persistableStateAttachmentState as unknown as PatternAnalysisProps), + ...(rawState as unknown as PatternAnalysisProps), timeRange, }; @@ -57,11 +56,7 @@ export const initComponent = memoize( ); }, - (prevProps, nextProps) => - deepEqual( - prevProps.persistableStateAttachmentState, - nextProps.persistableStateAttachmentState - ) + (prevProps, nextProps) => deepEqual(prevProps.data.state, nextProps.data.state) ); } ); diff --git a/x-pack/platform/plugins/shared/aiops/public/cases/log_rate_analysis_attachment.tsx b/x-pack/platform/plugins/shared/aiops/public/cases/log_rate_analysis_attachment.tsx index 6639fd4852f09..df6b26df947f6 100644 --- a/x-pack/platform/plugins/shared/aiops/public/cases/log_rate_analysis_attachment.tsx +++ b/x-pack/platform/plugins/shared/aiops/public/cases/log_rate_analysis_attachment.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { memoize } from 'lodash'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; +import type { UnifiedValueAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; import type { TimeRange } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -20,15 +20,14 @@ import type { export const initComponent = memoize( (fieldFormats: FieldFormatsStart, LogRateAnalysisComponent: LogRateAnalysisEmbeddableWrapper) => { - return React.memo((props: PersistableStateAttachmentViewProps) => { - const { persistableStateAttachmentState } = props; + return React.memo((props: UnifiedValueAttachmentViewProps) => { + const rawState = props.data.state as Record; const dataFormatter = fieldFormats.deserialize({ id: FIELD_FORMAT_IDS.DATE, }); - const rawState = persistableStateAttachmentState as unknown as Record; const timeRange = (rawState.time_range ?? rawState.timeRange) as TimeRange; const inputProps = { - ...(persistableStateAttachmentState as unknown as LogRateAnalysisEmbeddableWrapperProps), + ...(rawState as unknown as LogRateAnalysisEmbeddableWrapperProps), timeRange, }; diff --git a/x-pack/platform/plugins/shared/aiops/public/cases/register_cases.tsx b/x-pack/platform/plugins/shared/aiops/public/cases/register_cases.tsx index b98aca16fee7d..d109472e98be3 100644 --- a/x-pack/platform/plugins/shared/aiops/public/cases/register_cases.tsx +++ b/x-pack/platform/plugins/shared/aiops/public/cases/register_cases.tsx @@ -9,16 +9,19 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { CasesPublicSetup } from '@kbn/cases-plugin/public'; +import { + AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE, + AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE, + AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE, +} from '@kbn/cases-plugin/common'; import type { CoreStart } from '@kbn/core/public'; -import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '@kbn/aiops-change-point-detection/constants'; -import { CASES_ATTACHMENT_LOG_PATTERN } from '@kbn/aiops-log-pattern-analysis/constants'; -import { CASES_ATTACHMENT_LOG_RATE_ANALYSIS } from '@kbn/aiops-log-rate-analysis/constants'; import { getChangePointDetectionComponent, getLogRateAnalysisEmbeddableWrapperComponent, getPatternAnalysisComponent, } from '../shared_components'; import type { AiopsPluginStartDeps } from '../types'; +import { casesSchemaValidator } from '../../common/utils'; export function registerCases( cases: CasesPublicSetup, @@ -27,8 +30,8 @@ export function registerCases( ) { const ChangePointDetectionComponent = getChangePointDetectionComponent(coreStart, pluginStart); - cases.attachmentFramework.registerPersistableState({ - id: CASES_ATTACHMENT_CHANGE_POINT_CHART, + cases.attachmentFramework.registerUnified({ + id: AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE, icon: 'machineLearningApp', displayName: i18n.translate('xpack.aiops.changePointDetection.cases.attachmentName', { defaultMessage: 'Change point chart', @@ -57,12 +60,13 @@ export function registerCases( /> ), }), + schemaValidator: casesSchemaValidator, }); const LogPatternAttachmentComponent = getPatternAnalysisComponent(coreStart, pluginStart); - cases.attachmentFramework.registerPersistableState({ - id: CASES_ATTACHMENT_LOG_PATTERN, + cases.attachmentFramework.registerUnified({ + id: AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE, icon: 'machineLearningApp', displayName: i18n.translate('xpack.aiops.logPatternAnalysis.cases.attachmentName', { defaultMessage: 'Log pattern analysis', @@ -89,6 +93,7 @@ export function registerCases( /> ), }), + schemaValidator: casesSchemaValidator, }); const LogRateAnalysisEmbeddableWrapperComponent = getLogRateAnalysisEmbeddableWrapperComponent( @@ -96,8 +101,8 @@ export function registerCases( pluginStart ); - cases.attachmentFramework.registerPersistableState({ - id: CASES_ATTACHMENT_LOG_RATE_ANALYSIS, + cases.attachmentFramework.registerUnified({ + id: AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE, icon: 'machineLearningApp', displayName: i18n.translate('xpack.aiops.logRateAnalysis.cases.attachmentName', { defaultMessage: 'Log rate analysis', @@ -129,5 +134,6 @@ export function registerCases( /> ), }), + schemaValidator: casesSchemaValidator, }); } diff --git a/x-pack/platform/plugins/shared/aiops/public/hooks/use_cases_modal.ts b/x-pack/platform/plugins/shared/aiops/public/hooks/use_cases_modal.ts index 8a9aa5ed7ef74..44da5e0dd6fd8 100644 --- a/x-pack/platform/plugins/shared/aiops/public/hooks/use_cases_modal.ts +++ b/x-pack/platform/plugins/shared/aiops/public/hooks/use_cases_modal.ts @@ -7,8 +7,15 @@ import { useCallback, useMemo } from 'react'; import { stringHash } from '@kbn/ml-string-hash'; -import { AttachmentType } from '@kbn/cases-plugin/common'; +import { + AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE, + AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE, + AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE, +} from '@kbn/cases-plugin/common'; import { i18n } from '@kbn/i18n'; +import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '@kbn/aiops-change-point-detection/constants'; +import { EMBEDDABLE_PATTERN_ANALYSIS_TYPE } from '@kbn/aiops-log-pattern-analysis/constants'; +import { EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE } from '@kbn/aiops-log-rate-analysis/constants'; import type { ChangePointEmbeddableState } from '../../common/embeddables/change_point_chart/types'; import type { EmbeddableChangePointChartType } from '../embeddables/change_point_chart/embeddable_change_point_chart_factory'; import { useAiopsAppContext } from './use_aiops_app_context'; @@ -31,6 +38,12 @@ type EmbeddableRuntimeState = ? LogRateAnalysisEmbeddableState : never; +const attachmentTypeByEmbeddableType: Record = { + [EMBEDDABLE_CHANGE_POINT_CHART_TYPE]: AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE, + [EMBEDDABLE_PATTERN_ANALYSIS_TYPE]: AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE, + [EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE]: AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE, +}; + /** * Returns a callback for opening the cases modal with provided attachment state. */ @@ -68,11 +81,10 @@ export const useCasesModal = ( selectCaseModal.open({ getAttachments: () => [ { - type: AttachmentType.persistableState, - persistableStateAttachmentTypeId: embeddableType, - persistableStateAttachmentState: JSON.parse( - JSON.stringify(persistableStateAttachmentState) - ), + type: attachmentTypeByEmbeddableType[embeddableType], + data: { + state: JSON.parse(JSON.stringify(persistableStateAttachmentState)), + }, }, ], }); diff --git a/x-pack/platform/plugins/shared/aiops/server/plugin.ts b/x-pack/platform/plugins/shared/aiops/server/plugin.ts index 7e56679967301..1f8f4e6fe9182 100755 --- a/x-pack/platform/plugins/shared/aiops/server/plugin.ts +++ b/x-pack/platform/plugins/shared/aiops/server/plugin.ts @@ -31,7 +31,7 @@ import type { import { defineRoute as defineLogRateAnalysisFieldCandidatesRoute } from './routes/log_rate_analysis_field_candidates/define_route'; import { defineRoute as defineLogRateAnalysisRoute } from './routes/log_rate_analysis/define_route'; import { defineRoute as defineCategorizationFieldValidationRoute } from './routes/categorization_field_validation/define_route'; -import { registerCasesPersistableState } from './register_cases'; +import { registerCaseAttachments } from './register_cases'; import type { ConfigSchema } from './config_schema'; import { setupCapabilities } from './lib/capabilities'; import { transformIn as changePointTransformIn } from '../common/embeddables/change_point_chart/transform_in'; @@ -70,7 +70,7 @@ export class AiopsPlugin aiopsLicense.isActivePlatinumLicense = isActiveLicense('platinum', license); if (aiopsLicense.isActivePlatinumLicense) { - registerCasesPersistableState(plugins.cases, this.logger); + registerCaseAttachments(plugins.cases, this.logger); } }); diff --git a/x-pack/platform/plugins/shared/aiops/server/register_cases.ts b/x-pack/platform/plugins/shared/aiops/server/register_cases.ts index ac126a74a5a21..dcc19c45eacc3 100644 --- a/x-pack/platform/plugins/shared/aiops/server/register_cases.ts +++ b/x-pack/platform/plugins/shared/aiops/server/register_cases.ts @@ -7,21 +7,27 @@ import type { Logger } from '@kbn/core/server'; import type { CasesServerSetup } from '@kbn/cases-plugin/server'; -import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '@kbn/aiops-change-point-detection/constants'; -import { CASES_ATTACHMENT_LOG_PATTERN } from '@kbn/aiops-log-pattern-analysis/constants'; -import { CASES_ATTACHMENT_LOG_RATE_ANALYSIS } from '@kbn/aiops-log-rate-analysis/constants'; +import { + AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE, + AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE, + AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE, +} from '@kbn/cases-plugin/common'; +import { casesSchemaValidator } from '../common/utils'; -export function registerCasesPersistableState(cases: CasesServerSetup | undefined, logger: Logger) { +export function registerCaseAttachments(cases: CasesServerSetup | undefined, logger: Logger) { if (cases) { try { - cases.attachmentFramework.registerPersistableState({ - id: CASES_ATTACHMENT_CHANGE_POINT_CHART, + cases.attachmentFramework.registerUnified({ + id: AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE, + schemaValidator: casesSchemaValidator, }); - cases.attachmentFramework.registerPersistableState({ - id: CASES_ATTACHMENT_LOG_PATTERN, + cases.attachmentFramework.registerUnified({ + id: AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE, + schemaValidator: casesSchemaValidator, }); - cases.attachmentFramework.registerPersistableState({ - id: CASES_ATTACHMENT_LOG_RATE_ANALYSIS, + cases.attachmentFramework.registerUnified({ + id: AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE, + schemaValidator: casesSchemaValidator, }); } catch (error) { logger.warn(`AIOPs failed to register cases persistable state`); diff --git a/x-pack/platform/plugins/shared/cases/common/constants/attachments.ts b/x-pack/platform/plugins/shared/cases/common/constants/attachments.ts index f7b86e83441c2..795a10319369b 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/attachments.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/attachments.ts @@ -11,6 +11,13 @@ export const COMMENT_ATTACHMENT_TYPE = 'comment'; export const SECURITY_EVENT_ATTACHMENT_TYPE = 'security.event'; export const LENS_ATTACHMENT_TYPE = 'lens'; +export const ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE = 'ml.anomaly_swimlane'; +export const ML_ANOMALY_CHARTS_ATTACHMENT_TYPE = 'ml.anomaly_charts'; +export const ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE = 'ml.single_metric_viewer'; +export const AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE = 'aiops.change_point_chart'; +export const AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE = 'aiops.pattern_analysis'; +export const AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE = 'aiops.log_rate_analysis'; + // ----------------Legacy attachment types------------------------- export const LEGACY_ACTIONS_TYPE = 'actions'; export const LEGACY_ALERT_TYPE = 'alert'; @@ -20,6 +27,12 @@ export const LEGACY_PERSISTABLE_STATE_TYPE = 'persistableState'; export const LEGACY_USER_TYPE = 'user'; export const LEGACY_LENS_ATTACHMENT_TYPE = '.lens'; +export const LEGACY_ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE = 'ml_anomaly_swimlane'; +export const LEGACY_ML_ANOMALY_CHARTS_ATTACHMENT_TYPE = 'ml_anomaly_charts'; +export const LEGACY_ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE = 'ml_single_metric_viewer'; +export const LEGACY_AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE = 'aiopsChangePointChart'; +export const LEGACY_AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE = 'aiopsPatternAnalysisEmbeddable'; +export const LEGACY_AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE = 'aiopsLogRateAnalysisEmbeddable'; export const LEGACY_ATTACHMENT_TYPES = new Set([ LEGACY_ACTIONS_TYPE, @@ -37,10 +50,22 @@ export const UNIFIED_ATTACHMENT_TYPES = new Set([ export const PERSISTABLE_STATE_LEGACY_TO_UNIFIED_MAP: Record = { [LEGACY_LENS_ATTACHMENT_TYPE]: LENS_ATTACHMENT_TYPE, + [LEGACY_ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE]: ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE, + [LEGACY_ML_ANOMALY_CHARTS_ATTACHMENT_TYPE]: ML_ANOMALY_CHARTS_ATTACHMENT_TYPE, + [LEGACY_ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE]: ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE, + [LEGACY_AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE]: AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE, + [LEGACY_AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE]: AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE, + [LEGACY_AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE]: AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE, } as const; export const PERSISTABLE_STATE_UNIFIED_TO_LEGACY_MAP: Record = { [LENS_ATTACHMENT_TYPE]: LEGACY_LENS_ATTACHMENT_TYPE, + [ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE]: LEGACY_ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE, + [ML_ANOMALY_CHARTS_ATTACHMENT_TYPE]: LEGACY_ML_ANOMALY_CHARTS_ATTACHMENT_TYPE, + [ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE]: LEGACY_ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE, + [AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE]: LEGACY_AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE, + [AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE]: LEGACY_AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE, + [AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE]: LEGACY_AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE, } as const; export const PERSISTABLE_ATTACHMENT_TYPES = new Set( diff --git a/x-pack/platform/plugins/shared/cases/common/index.ts b/x-pack/platform/plugins/shared/cases/common/index.ts index d382b4ae61cd8..b64fa7a9369f7 100644 --- a/x-pack/platform/plugins/shared/cases/common/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/index.ts @@ -60,6 +60,12 @@ export { ASSIGN_CASE_CAPABILITY, SECURITY_EVENT_ATTACHMENT_TYPE, MANAGE_TEMPLATES_CAPABILITY, + ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE, + ML_ANOMALY_CHARTS_ATTACHMENT_TYPE, + ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE, + AIOPS_CHANGE_POINT_CHART_ATTACHMENT_TYPE, + AIOPS_PATTERN_ANALYSIS_ATTACHMENT_TYPE, + AIOPS_LOG_RATE_ANALYSIS_ATTACHMENT_TYPE, } from './constants'; export type { AttachmentAttributes } from './types/domain'; diff --git a/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts b/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts index 0843ae5505af1..850f0fb6beee1 100644 --- a/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/client/attachment_framework/types.ts @@ -116,6 +116,7 @@ export interface AttachmentType { getAttachmentTabViewObject?: ( props?: CommonAttachmentTabViewProps ) => AttachmentTabViewObject; + schemaValidator?: (data: unknown) => void; } export type ExternalReferenceAttachmentType = AttachmentType; diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.test.tsx index d0424a17185b4..3115c39ca7059 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.test.tsx @@ -24,7 +24,6 @@ import { getExternalReferenceUserAction, getHostIsolationUserAction, getMultipleAlertsUserAction, - getPersistableStateAttachment, getPersistableStateUserAction, getUserAction, hostIsolationComment, @@ -36,7 +35,6 @@ import { getMockBuilderArgs, getMockCommentRenderingContext } from '../mock'; import { CommentRenderingProvider } from './comment_rendering_context'; import { useCaseViewNavigation, useCaseViewParams } from '../../../common/navigation'; import { ExternalReferenceAttachmentTypeRegistry } from '../../../client/attachment_framework/external_reference_registry'; -import { PersistableStateAttachmentTypeRegistry } from '../../../client/attachment_framework/persistable_state_registry'; import { userProfiles } from '../../../containers/user_profiles/api.mock'; import { AttachmentActionType } from '../../../client/attachment_framework/types'; import { UnifiedAttachmentTypeRegistry } from '../../../client/attachment_framework/unified_attachment_registry'; @@ -190,75 +188,26 @@ describe('createCommentUserActionBuilder', () => { expect(screen.getByText('removed attachment')).toBeInTheDocument(); }); - it('renders correctly when deleting a persistable state attachment', async () => { - const userAction = getPersistableStateUserAction({ action: UserActionActions.delete }); - const builder = createCommentUserActionBuilder({ - ...builderArgs, - attachments: [basicCommentUnified], - userAction, - }); - - const createdUserAction = builder.build(); - renderWithTestingProviders(); - - expect(screen.getByText('removed attachment')).toBeInTheDocument(); - }); - - it('renders correctly when deleting a persistable state attachment with getAttachmentRemovalObject defined', async () => { - const getAttachmentRemovalObject = jest.fn().mockReturnValue({ - event: 'removed my own attachment', - }); - - const persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(); - const attachment = getPersistableStateAttachment(); - persistableStateAttachmentTypeRegistry.register({ - ...attachment, - getAttachmentRemovalObject, - }); - - const userAction = getPersistableStateUserAction({ action: UserActionActions.delete }); - const builder = createCommentUserActionBuilder({ - ...builderArgs, - persistableStateAttachmentTypeRegistry, - userAction, - caseData: { - ...builderArgs.caseData, - comments: [persistableStateAttachment], - }, - }); - - const createdUserAction = builder.build(); - renderWithTestingProviders(); - - expect(screen.getByText('removed my own attachment')).toBeInTheDocument(); - expect(getAttachmentRemovalObject).toBeCalledWith({ - caseData: { - id: 'basic-case-id', - title: 'Another horrible breach!!', - }, - persistableStateAttachmentTypeId: '.test', - persistableStateAttachmentState: { - test_foo: 'foo', + it('renders correctly when deleting a unified value attachment', async () => { + const userAction = getPersistableStateUserAction({ + action: UserActionActions.delete, + payload: { + comment: { + type: 'lens', + data: { state: { test_foo: 'foo' } }, + owner: 'securitySolution', + }, }, }); - }); - - it('renders correctly when deleting a persistable state attachment without getAttachmentRemovalObject defined', async () => { - const persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(); - const attachment = getPersistableStateAttachment(); - persistableStateAttachmentTypeRegistry.register(attachment); - - const userAction = getPersistableStateUserAction({ action: UserActionActions.delete }); const builder = createCommentUserActionBuilder({ ...builderArgs, - persistableStateAttachmentTypeRegistry, userAction, }); const createdUserAction = builder.build(); renderWithTestingProviders(); - expect(screen.getByText('removed attachment')).toBeInTheDocument(); + expect(screen.getByText('removed comment')).toBeInTheDocument(); }); it('renders correctly when deleting a unified event attachment', async () => { @@ -619,8 +568,8 @@ describe('createCommentUserActionBuilder', () => { const unifiedEventAttachment = { id: eventComment.id, type: 'security.event', - attachmentId: eventComment.eventId, - metadata: { index: eventComment.index }, + attachmentId: 'event-id-1', + metadata: { index: 'event-index-1' }, owner: eventComment.owner, createdAt: eventComment.createdAt, createdBy: eventComment.createdBy, @@ -784,12 +733,11 @@ describe('createCommentUserActionBuilder', () => { }); }); - describe('Persistable state', () => { - it('renders correctly a persistable state attachment', async () => { + describe('Unified value attachments', () => { + it('renders correctly a unified value attachment', async () => { const MockComponent = jest.fn((props) => { - return ( -
- ); + const state = props.data.state as { test_foo: string }; + return
; }); const SpyLazyFactory = jest.fn(() => { @@ -800,17 +748,25 @@ describe('createCommentUserActionBuilder', () => { }); }); - const persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(); - persistableStateAttachmentTypeRegistry.register( - getPersistableStateAttachment({ + const unifiedAttachmentTypeRegistry = new UnifiedAttachmentTypeRegistry(); + unifiedAttachmentTypeRegistry.register(getCommentAttachmentType()); + unifiedAttachmentTypeRegistry.register({ + id: 'lens', + displayName: 'Lens', + icon: 'lensApp', + getAttachmentViewObject: () => ({ + event: 'added an embeddable', + timelineAvatar: 'lensApp', children: React.lazy(SpyLazyFactory), - }) - ); + }), + }); const userAction = getPersistableStateUserAction(); const attachment01 = { - ...persistableStateAttachment, - persistableStateAttachmentState: { test_foo: '01' }, + ...basicCommentUnified, + id: persistableStateAttachment.id, + type: 'lens', + data: { state: { test_foo: '01' } }, createdBy: { username: userProfiles[0].user.username, fullName: userProfiles[0].user.full_name, @@ -818,12 +774,10 @@ describe('createCommentUserActionBuilder', () => { profileUid: userProfiles[0].uid, }, }; + const builder = createCommentUserActionBuilder({ ...builderArgs, - persistableStateAttachmentTypeRegistry, - caseData: { - ...builderArgs.caseData, - }, + unifiedAttachmentTypeRegistry, attachments: [attachment01], userAction, }); @@ -839,7 +793,7 @@ describe('createCommentUserActionBuilder', () => { expect(MockComponent).toHaveBeenCalledTimes(1); expect(SpyLazyFactory).toHaveBeenCalledTimes(1); - expect(screen.getByTestId('comment-persistableState-.test')).toBeInTheDocument(); + expect(screen.getByTestId('comment-lens-lens')).toBeInTheDocument(); expect(screen.getByTestId('copy-link-persistable-state-comment-id')).toBeInTheDocument(); expect(screen.getByTestId('case-user-profile-avatar-damaged_raccoon')).toBeInTheDocument(); expect(screen.getByText('added an embeddable')).toBeInTheDocument(); @@ -847,15 +801,12 @@ describe('createCommentUserActionBuilder', () => { unmount(); const attachment02 = { - ...persistableStateAttachment, - persistableStateAttachmentState: { test_foo: '02' }, + ...attachment01, + data: { state: { test_foo: '02' } }, }; const updateBuilder = createCommentUserActionBuilder({ ...builderArgs, - persistableStateAttachmentTypeRegistry, - caseData: { - ...builderArgs.caseData, - }, + unifiedAttachmentTypeRegistry, attachments: [attachment02], userAction, }); @@ -870,48 +821,57 @@ describe('createCommentUserActionBuilder', () => { expect(SpyLazyFactory).toHaveBeenCalledTimes(1); }); - it('renders correctly if the reference is not registered', async () => { - const persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(); - + it('does not render if the attachment type is not registered', async () => { const userAction = getPersistableStateUserAction(); const builder = createCommentUserActionBuilder({ ...builderArgs, - persistableStateAttachmentTypeRegistry, - caseData: { - ...builderArgs.caseData, - }, - attachments: [persistableStateAttachment], + attachments: [ + { + ...basicCommentUnified, + id: persistableStateAttachment.id, + type: 'lens', + data: { state: { test_foo: 'foo' } }, + }, + ], userAction, }); const createdUserAction = builder.build(); - renderWithTestingProviders(); - - expect(screen.getByTestId('comment-persistableState-not-found')).toBeInTheDocument(); - expect(screen.getByText('added an attachment of type')).toBeInTheDocument(); - expect(screen.getByText('Attachment type is not registered')).toBeInTheDocument(); + expect(createdUserAction).toEqual([]); }); it('deletes the attachment correctly', async () => { - const attachment = getPersistableStateAttachment(); - const persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(); - persistableStateAttachmentTypeRegistry.register(attachment); + const unifiedAttachmentTypeRegistry = new UnifiedAttachmentTypeRegistry(); + unifiedAttachmentTypeRegistry.register(getCommentAttachmentType()); + unifiedAttachmentTypeRegistry.register({ + id: 'lens', + displayName: 'Lens', + icon: 'lensApp', + getAttachmentViewObject: () => ({ + event: 'added an embeddable', + timelineAvatar: 'lensApp', + }), + }); const userAction = getPersistableStateUserAction(); const builder = createCommentUserActionBuilder({ ...builderArgs, - persistableStateAttachmentTypeRegistry, - caseData: { - ...builderArgs.caseData, - }, - attachments: [persistableStateAttachment], + unifiedAttachmentTypeRegistry, + attachments: [ + { + ...basicCommentUnified, + id: persistableStateAttachment.id, + type: 'lens', + data: { state: { test_foo: 'foo' } }, + }, + ], userAction, }); const createdUserAction = builder.build(); renderWithTestingProviders(); - expect(screen.getByTestId('comment-persistableState-.test')).toBeInTheDocument(); + expect(screen.getByTestId('comment-lens-lens')).toBeInTheDocument(); await deleteAttachment('trash', 'Delete'); @@ -922,6 +882,16 @@ describe('createCommentUserActionBuilder', () => { ); }); }); + + it('does not render legacy persistable state attachments', async () => { + const builder = createCommentUserActionBuilder({ + ...builderArgs, + attachments: [persistableStateAttachment], + userAction: getPersistableStateUserAction(), + }); + + expect(builder.build()).toEqual([]); + }); }); it('shows correctly the visible primary actions', async () => { diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.tsx index fa4d3e249688d..7b0f2229e6e3c 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.tsx @@ -19,7 +19,6 @@ import { createUnifiedAttachmentUserActionBuilder } from './unified_attachment'; import { createAlertAttachmentUserActionBuilder } from '../../attachments/alert/alert'; import { createActionAttachmentUserActionBuilder } from '../../attachments/host_isolation/actions'; import { createExternalReferenceAttachmentUserActionBuilder } from './external_reference'; -import { createPersistableStateAttachmentUserActionBuilder } from './persistable_state'; import type { AttachmentType as AttachmentFrameworkAttachmentType } from '../../../client/attachment_framework/types'; import { isLegacyAttachmentRequest, @@ -34,7 +33,6 @@ interface DeleteLabelTitle { userAction: SnakeToCamelCase; caseData: UserActionBuilderArgs['caseData']; externalReferenceAttachmentTypeRegistry: UserActionBuilderArgs['externalReferenceAttachmentTypeRegistry']; - persistableStateAttachmentTypeRegistry: UserActionBuilderArgs['persistableStateAttachmentTypeRegistry']; unifiedAttachmentTypeRegistry: UserActionBuilderArgs['unifiedAttachmentTypeRegistry']; } @@ -42,7 +40,6 @@ const getDeleteLabelTitle = ({ userAction, caseData, externalReferenceAttachmentTypeRegistry, - persistableStateAttachmentTypeRegistry, unifiedAttachmentTypeRegistry, }: DeleteLabelTitle) => { const { comment } = userAction.payload; @@ -66,18 +63,6 @@ const getDeleteLabelTitle = ({ }), }); } - - if (comment.type === AttachmentType.persistableState) { - return getDeleteLabelFromRegistry({ - caseData, - registry: persistableStateAttachmentTypeRegistry, - getId: () => comment.persistableStateAttachmentTypeId, - getAttachmentProps: () => ({ - persistableStateAttachmentTypeId: comment.persistableStateAttachmentTypeId, - persistableStateAttachmentState: comment.persistableStateAttachmentState, - }), - }); - } } if (isUnifiedReferenceAttachmentRequest(comment)) { return getDeleteLabelFromRegistry({ @@ -132,7 +117,6 @@ const getDeleteCommentUserAction = ({ userProfiles, caseData, externalReferenceAttachmentTypeRegistry, - persistableStateAttachmentTypeRegistry, unifiedAttachmentTypeRegistry, handleOutlineComment, }: { @@ -142,7 +126,6 @@ const getDeleteCommentUserAction = ({ | 'handleOutlineComment' | 'userProfiles' | 'externalReferenceAttachmentTypeRegistry' - | 'persistableStateAttachmentTypeRegistry' | 'unifiedAttachmentTypeRegistry' | 'caseData' >): EuiCommentProps[] => { @@ -150,7 +133,6 @@ const getDeleteCommentUserAction = ({ userAction, caseData, externalReferenceAttachmentTypeRegistry, - persistableStateAttachmentTypeRegistry, unifiedAttachmentTypeRegistry, }); @@ -171,7 +153,6 @@ const getCreateCommentUserAction = ({ userProfiles, caseData, externalReferenceAttachmentTypeRegistry, - persistableStateAttachmentTypeRegistry, unifiedAttachmentTypeRegistry, attachment, manageMarkdownEditIds, @@ -190,7 +171,11 @@ const getCreateCommentUserAction = ({ attachment: AttachmentUIV2; } & Omit< UserActionBuilderArgs, - 'comments' | 'index' | 'handleOutlineComment' | 'currentUserProfile' + | 'comments' + | 'index' + | 'handleOutlineComment' + | 'currentUserProfile' + | 'persistableStateAttachmentTypeRegistry' >): EuiCommentProps[] => { if (isLegacyAttachmentRequest(attachment)) { switch (attachment.type) { @@ -233,19 +218,6 @@ const getCreateCommentUserAction = ({ return externalReferenceBuilder.build(); - case AttachmentType.persistableState: - const persistableBuilder = createPersistableStateAttachmentUserActionBuilder({ - userAction, - userProfiles, - attachment, - persistableStateAttachmentTypeRegistry, - caseData, - isLoading: loadingCommentIds.includes(attachment.id), - handleDeleteComment, - }); - - return persistableBuilder.build(); - default: return []; } @@ -286,7 +258,6 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({ casesConfiguration, userProfiles, externalReferenceAttachmentTypeRegistry, - persistableStateAttachmentTypeRegistry, unifiedAttachmentTypeRegistry, userAction, manageMarkdownEditIds, @@ -314,7 +285,6 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({ handleOutlineComment, userProfiles, externalReferenceAttachmentTypeRegistry, - persistableStateAttachmentTypeRegistry, unifiedAttachmentTypeRegistry, }); } @@ -332,7 +302,6 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({ userProfiles, userAction: attachmentUserAction, externalReferenceAttachmentTypeRegistry, - persistableStateAttachmentTypeRegistry, unifiedAttachmentTypeRegistry, attachment, manageMarkdownEditIds, diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/persistable_state.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/persistable_state.tsx deleted file mode 100644 index 3c358925f15a1..0000000000000 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/persistable_state.tsx +++ /dev/null @@ -1,48 +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 type { PersistableStateAttachment } from '../../../../common/types/domain'; -import type { UserActionBuilder, UserActionBuilderArgs } from '../types'; -import type { SnakeToCamelCase } from '../../../../common/types'; -import { createRegisteredAttachmentUserActionBuilder } from './registered_attachments'; - -type BuilderArgs = Pick< - UserActionBuilderArgs, - | 'userAction' - | 'persistableStateAttachmentTypeRegistry' - | 'caseData' - | 'handleDeleteComment' - | 'userProfiles' -> & { - attachment: SnakeToCamelCase; - isLoading: boolean; -}; - -export const createPersistableStateAttachmentUserActionBuilder = ({ - userAction, - userProfiles, - attachment, - persistableStateAttachmentTypeRegistry, - caseData, - isLoading, - handleDeleteComment, -}: BuilderArgs): ReturnType => { - return createRegisteredAttachmentUserActionBuilder({ - userAction, - userProfiles, - attachment, - registry: persistableStateAttachmentTypeRegistry, - caseData, - handleDeleteComment, - isLoading, - getId: () => attachment.persistableStateAttachmentTypeId, - getAttachmentViewProps: () => ({ - persistableStateAttachmentTypeId: attachment.persistableStateAttachmentTypeId, - persistableStateAttachmentState: attachment.persistableStateAttachmentState, - }), - }); -}; diff --git a/x-pack/platform/plugins/shared/ml/common/constants/cases.ts b/x-pack/platform/plugins/shared/ml/common/constants/cases.ts deleted file mode 100644 index c8089ebaf5dbd..0000000000000 --- a/x-pack/platform/plugins/shared/ml/common/constants/cases.ts +++ /dev/null @@ -1,10 +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. - */ - -export const CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE = 'ml_anomaly_swimlane' as const; -export const CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS = 'ml_anomaly_charts' as const; -export const CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER = 'ml_single_metric_viewer' as const; diff --git a/x-pack/platform/plugins/shared/ml/common/util/cases_utils.ts b/x-pack/platform/plugins/shared/ml/common/util/cases_utils.ts new file mode 100644 index 0000000000000..d55a586551bd1 --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/common/util/cases_utils.ts @@ -0,0 +1,18 @@ +/* + * 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 { isPlainObject } from 'lodash'; + +export const casesSchemaValidator = (data: unknown) => { + if (!isPlainObject(data) || data === null) { + throw new Error('Persistable attachment data must be an object'); + } + + const state = (data as Record).state; + if (!isPlainObject(state) || state === null) { + throw new Error('Persistable attachment data must include object "state"'); + } +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/contexts/kibana/use_cases_modal.ts b/x-pack/platform/plugins/shared/ml/public/application/contexts/kibana/use_cases_modal.ts index f24a541884555..b184d333d3873 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/contexts/kibana/use_cases_modal.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/contexts/kibana/use_cases_modal.ts @@ -7,10 +7,25 @@ import { useCallback, useMemo } from 'react'; import { stringHash } from '@kbn/ml-string-hash'; -import { AttachmentType } from '@kbn/cases-plugin/common'; +import { + ML_ANOMALY_CHARTS_ATTACHMENT_TYPE, + ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE, + ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE, +} from '@kbn/cases-plugin/common'; import { i18n } from '@kbn/i18n'; import { useMlKibana } from './kibana_context'; import type { MappedEmbeddableTypeOf, MlEmbeddableTypes } from '../../../embeddables'; +import { + ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, + ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE, + ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, +} from '../../../embeddables'; + +const attachmentTypeByEmbeddableType: Record = { + [ANOMALY_SWIMLANE_EMBEDDABLE_TYPE]: ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE, + [ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE]: ML_ANOMALY_CHARTS_ATTACHMENT_TYPE, + [ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE]: ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE, +}; /** * Returns a callback for opening the cases modal with provided attachment state. @@ -37,11 +52,11 @@ export const useCasesModal = ( }); return useCallback( - (persistableState: Partial, 'id'>>) => { - const persistableStateAttachmentState = { - ...persistableState, + (state: Partial, 'id'>>) => { + const attachmentState = { + ...state, // Creates unique id based on the input - id: stringHash(JSON.stringify(persistableState)).toString(), + id: stringHash(JSON.stringify(state)).toString(), }; if (!selectCaseModal) { @@ -51,12 +66,10 @@ export const useCasesModal = ( selectCaseModal.open({ getAttachments: () => [ { - type: AttachmentType.persistableState, - persistableStateAttachmentTypeId: embeddableType, - // TODO Cases: improve type for persistableStateAttachmentState with io-ts - persistableStateAttachmentState: JSON.parse( - JSON.stringify(persistableStateAttachmentState) - ), + type: attachmentTypeByEmbeddableType[embeddableType], + data: { + state: JSON.parse(JSON.stringify(attachmentState)), + }, }, ], }); diff --git a/x-pack/platform/plugins/shared/ml/public/cases/anomaly_charts_attachments.tsx b/x-pack/platform/plugins/shared/ml/public/cases/anomaly_charts_attachments.tsx index 0696bffbe9613..3c54bb14e4e99 100644 --- a/x-pack/platform/plugins/shared/ml/public/cases/anomaly_charts_attachments.tsx +++ b/x-pack/platform/plugins/shared/ml/public/cases/anomaly_charts_attachments.tsx @@ -14,7 +14,7 @@ import { BehaviorSubject } from 'rxjs'; import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import { transformTimeRangeOut } from '@kbn/presentation-publishing'; -import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; +import type { UnifiedValueAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; import { EuiDescriptionList, htmlIdGenerator } from '@elastic/eui'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; @@ -96,16 +96,15 @@ function isValidTimeRange(arg: unknown): arg is TimeRange { export const initializeAnomalyChartsAttachment = memoize( (fieldFormats: FieldFormatsStart, services: AnomalyChartsEmbeddableServices) => { return React.memo( - (props: PersistableStateAttachmentViewProps) => { - const { persistableStateAttachmentState } = props; + (props: UnifiedValueAttachmentViewProps) => { + const attachmentState = props.data.state as Record; const dataFormatter = fieldFormats.deserialize({ id: FIELD_FORMAT_IDS.DATE, }); const inputProps = transformTimeRangeOut( - persistableStateAttachmentState as unknown as AnomalyChartsAttachmentState & - Record + attachmentState as unknown as AnomalyChartsAttachmentState & Record ); const descriptions = useMemo(() => { @@ -162,11 +161,7 @@ export const initializeAnomalyChartsAttachment = memoize( ); }, - (prevProps, nextProps) => - deepEqual( - prevProps.persistableStateAttachmentState, - nextProps.persistableStateAttachmentState - ) + (prevProps, nextProps) => deepEqual(prevProps.data.state, nextProps.data.state) ); } ); diff --git a/x-pack/platform/plugins/shared/ml/public/cases/anomaly_swim_lane_attachment.tsx b/x-pack/platform/plugins/shared/ml/public/cases/anomaly_swim_lane_attachment.tsx index 0864ba92e6c0f..8d302e4b2b326 100644 --- a/x-pack/platform/plugins/shared/ml/public/cases/anomaly_swim_lane_attachment.tsx +++ b/x-pack/platform/plugins/shared/ml/public/cases/anomaly_swim_lane_attachment.tsx @@ -6,7 +6,7 @@ */ import { EuiDescriptionList } from '@elastic/eui'; -import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; +import type { UnifiedValueAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import { EmbeddableRenderer } from '@kbn/embeddable-plugin/public'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; @@ -15,23 +15,24 @@ import { transformTimeRangeOut } from '@kbn/presentation-publishing'; import deepEqual from 'fast-deep-equal'; import { memoize } from 'lodash'; import React from 'react'; -import { CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE } from '../../common/constants/cases'; import type { AnomalySwimLaneEmbeddableApi, AnomalySwimLaneEmbeddableState, } from '../embeddables/anomaly_swimlane/types'; +import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../embeddables/constants'; export const initComponent = memoize((fieldFormats: FieldFormatsStart) => { return React.memo( - (props: PersistableStateAttachmentViewProps) => { - const { persistableStateAttachmentState, caseData } = props; + (props: UnifiedValueAttachmentViewProps) => { + const { caseData } = props; + const attachmentState = props.data.state as Record; const dataFormatter = fieldFormats.deserialize({ id: FIELD_FORMAT_IDS.DATE, }); const inputProps = transformTimeRangeOut( - persistableStateAttachmentState as unknown as AnomalySwimLaneEmbeddableState + attachmentState as unknown as AnomalySwimLaneEmbeddableState ); const listItems = [ @@ -87,7 +88,7 @@ export const initComponent = memoize((fieldFormats: FieldFormatsStart) => { maybeId={inputProps.id} - type={CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE} + type={ANOMALY_SWIMLANE_EMBEDDABLE_TYPE} getParentApi={() => ({ getSerializedStateForChild: () => inputProps, executionContext: { @@ -100,10 +101,6 @@ export const initComponent = memoize((fieldFormats: FieldFormatsStart) => { ); }, - (prevProps, nextProps) => - deepEqual( - prevProps.persistableStateAttachmentState, - nextProps.persistableStateAttachmentState - ) + (prevProps, nextProps) => deepEqual(prevProps.data.state, nextProps.data.state) ); }); diff --git a/x-pack/platform/plugins/shared/ml/public/cases/register_anomaly_charts_attachment.tsx b/x-pack/platform/plugins/shared/ml/public/cases/register_anomaly_charts_attachment.tsx index f0fe06526fdc9..36ffa184f59c3 100644 --- a/x-pack/platform/plugins/shared/ml/public/cases/register_anomaly_charts_attachment.tsx +++ b/x-pack/platform/plugins/shared/ml/public/cases/register_anomaly_charts_attachment.tsx @@ -9,9 +9,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { CasesPublicSetup } from '@kbn/cases-plugin/public'; +import { ML_ANOMALY_CHARTS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; import type { CoreStart } from '@kbn/core/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; -import { CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS } from '../../common/constants/cases'; +import { casesSchemaValidator } from '../../common/util/cases_utils'; import type { MlStartDependencies } from '../plugin'; import { PLUGIN_ICON } from '../../common/constants/app'; import { getAnomalyChartsServiceDependencies } from '../embeddables/anomaly_charts/get_anomaly_charts_services_dependencies'; @@ -22,8 +23,8 @@ export function registerAnomalyChartsCasesAttachment( pluginStart: MlStartDependencies, usageCollection?: UsageCollectionSetup ) { - cases.attachmentFramework.registerPersistableState({ - id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS, + cases.attachmentFramework.registerUnified({ + id: ML_ANOMALY_CHARTS_ATTACHMENT_TYPE, icon: PLUGIN_ICON, displayName: i18n.translate('xpack.ml.cases.anomalyCharts.displayName', { defaultMessage: 'Anomaly charts', @@ -49,5 +50,6 @@ export function registerAnomalyChartsCasesAttachment( }; }), }), + schemaValidator: casesSchemaValidator, }); } diff --git a/x-pack/platform/plugins/shared/ml/public/cases/register_anomaly_swim_lane_attachment.tsx b/x-pack/platform/plugins/shared/ml/public/cases/register_anomaly_swim_lane_attachment.tsx index 1c8a19210282c..4ab1622f09193 100644 --- a/x-pack/platform/plugins/shared/ml/public/cases/register_anomaly_swim_lane_attachment.tsx +++ b/x-pack/platform/plugins/shared/ml/public/cases/register_anomaly_swim_lane_attachment.tsx @@ -6,19 +6,20 @@ */ import type { CasesPublicSetup } from '@kbn/cases-plugin/public'; +import { ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; +import { casesSchemaValidator } from '../../common/util/cases_utils'; import { PLUGIN_ICON } from '../../common/constants/app'; -import { CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE } from '../../common/constants/cases'; import type { MlStartDependencies } from '../plugin'; export function registerAnomalySwimLaneCasesAttachment( cases: CasesPublicSetup, pluginStart: MlStartDependencies ) { - cases.attachmentFramework.registerPersistableState({ - id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE, + cases.attachmentFramework.registerUnified({ + id: ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE, icon: PLUGIN_ICON, displayName: i18n.translate('xpack.ml.cases.anomalySwimLane.displayName', { defaultMessage: 'Anomaly swim lane', @@ -38,5 +39,6 @@ export function registerAnomalySwimLaneCasesAttachment( }; }), }), + schemaValidator: casesSchemaValidator, }); } diff --git a/x-pack/platform/plugins/shared/ml/public/cases/register_single_metric_viewer_attachment.tsx b/x-pack/platform/plugins/shared/ml/public/cases/register_single_metric_viewer_attachment.tsx index 61f6dd82518bb..7fdc2e5f82993 100644 --- a/x-pack/platform/plugins/shared/ml/public/cases/register_single_metric_viewer_attachment.tsx +++ b/x-pack/platform/plugins/shared/ml/public/cases/register_single_metric_viewer_attachment.tsx @@ -6,13 +6,14 @@ */ import type { CasesPublicSetup } from '@kbn/cases-plugin/public'; +import { ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; import { i18n } from '@kbn/i18n'; import type { CoreStart } from '@kbn/core/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; +import { casesSchemaValidator } from '../../common/util/cases_utils'; import { PLUGIN_ICON } from '../../common/constants/app'; -import { CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER } from '../../common/constants/cases'; import type { MlStartDependencies } from '../plugin'; import { getSingleMetricViewerComponent } from '../shared_components/single_metric_viewer'; import type { MlDependencies } from '../application/app'; @@ -24,8 +25,8 @@ export function registerSingleMetricViewerCasesAttachment( pluginStart: MlStartDependencies, usageCollection?: UsageCollectionSetup ) { - cases.attachmentFramework.registerPersistableState({ - id: CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER, + cases.attachmentFramework.registerUnified({ + id: ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE, icon: PLUGIN_ICON, displayName: i18n.translate('xpack.ml.cases.registerSingleMetricViewer.displayName', { defaultMessage: 'Single metric viewer', @@ -53,5 +54,6 @@ export function registerSingleMetricViewerCasesAttachment( }; }), }), + schemaValidator: casesSchemaValidator, }); } diff --git a/x-pack/platform/plugins/shared/ml/public/cases/single_metric_viewer_attachment.tsx b/x-pack/platform/plugins/shared/ml/public/cases/single_metric_viewer_attachment.tsx index e194690e26b40..96fd4854f4ad3 100644 --- a/x-pack/platform/plugins/shared/ml/public/cases/single_metric_viewer_attachment.tsx +++ b/x-pack/platform/plugins/shared/ml/public/cases/single_metric_viewer_attachment.tsx @@ -6,7 +6,7 @@ */ import { EuiDescriptionList } from '@elastic/eui'; -import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; +import type { UnifiedValueAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; import moment from 'moment'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; @@ -24,11 +24,12 @@ export const initComponent = memoize( SingleMetricViewerComponent: SingleMetricViewerSharedComponent ) => { return React.memo( - (props: PersistableStateAttachmentViewProps) => { - const { persistableStateAttachmentState, caseData } = props; + (props: UnifiedValueAttachmentViewProps) => { + const { caseData } = props; + const attachmentState = props.data.state as Record; const inputProps = transformTimeRangeOut( - persistableStateAttachmentState as unknown as SingleMetricViewerEmbeddableState + attachmentState as unknown as SingleMetricViewerEmbeddableState ); const dataFormatter = fieldFormats.deserialize({ @@ -86,11 +87,7 @@ export const initComponent = memoize( ); }, - (prevProps, nextProps) => - deepEqual( - prevProps.persistableStateAttachmentState, - nextProps.persistableStateAttachmentState - ) + (prevProps, nextProps) => deepEqual(prevProps.data.state, nextProps.data.state) ); } ); diff --git a/x-pack/platform/plugins/shared/ml/server/lib/register_cases.ts b/x-pack/platform/plugins/shared/ml/server/lib/register_cases.ts index 7a4925089b398..d6c0b65900d65 100644 --- a/x-pack/platform/plugins/shared/ml/server/lib/register_cases.ts +++ b/x-pack/platform/plugins/shared/ml/server/lib/register_cases.ts @@ -7,12 +7,13 @@ import type { Logger } from '@kbn/core/server'; import type { CasesServerSetup } from '@kbn/cases-plugin/server'; -import type { MlFeatures } from '../../common/constants/app'; import { - CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS, - CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE, - CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER, -} from '../../common/constants/cases'; + ML_ANOMALY_CHARTS_ATTACHMENT_TYPE, + ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE, + ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE, +} from '@kbn/cases-plugin/common'; +import type { MlFeatures } from '../../common/constants/app'; +import { casesSchemaValidator } from '../../common/util/cases_utils'; export function registerCasesPersistableState( cases: CasesServerSetup, @@ -21,31 +22,34 @@ export function registerCasesPersistableState( ) { if (enabledFeatures.ad === true) { try { - cases.attachmentFramework.registerPersistableState({ - id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE, + cases.attachmentFramework.registerUnified({ + id: ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE, + schemaValidator: casesSchemaValidator, }); } catch (error) { logger.warn( - `ML failed to register cases persistable state for ${CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE}` + `ML failed to register cases persistable state for ${ML_ANOMALY_SWIMLANE_ATTACHMENT_TYPE}` ); } try { - cases.attachmentFramework.registerPersistableState({ - id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS, + cases.attachmentFramework.registerUnified({ + id: ML_ANOMALY_CHARTS_ATTACHMENT_TYPE, + schemaValidator: casesSchemaValidator, }); } catch (error) { logger.warn( - `ML failed to register cases persistable state for ${CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS}` + `ML failed to register cases persistable state for ${ML_ANOMALY_CHARTS_ATTACHMENT_TYPE}` ); } try { - cases.attachmentFramework.registerPersistableState({ - id: CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER, + cases.attachmentFramework.registerUnified({ + id: ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE, + schemaValidator: casesSchemaValidator, }); } catch (error) { logger.warn( - `ML failed to register cases persistable state for ${CASE_ATTACHMENT_TYPE_ID_SINGLE_METRIC_VIEWER}` + `ML failed to register cases persistable state for ${ML_SINGLE_METRIC_VIEWER_ATTACHMENT_TYPE}` ); } } diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/attachments_framework/registered_unified_attachment_types.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/attachments_framework/registered_unified_attachment_types.ts index 0e32a9b38593f..194d7cfdde293 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/attachments_framework/registered_unified_attachment_types.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/attachments_framework/registered_unified_attachment_types.ts @@ -32,6 +32,12 @@ export default ({ getService }: FtrProviderContext): void => { lens: '45d27f9672c86ca48baf24ef1b04d4802555aee2', comment: '118a9989815489c24b81b160782015890ed2085e', 'security.event': '0337735d3e57178e44b426e41e616aae57fd794d', + aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', + ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147', + ml_anomaly_swimlane: 'a3517f3e53fb041e9cbb150477fb6ef0f731bd5f', + ml_single_metric_viewer: '8b9532b0a40dfdfa282e262949b82cc1a643147c', + aiopsPatternAnalysisEmbeddable: '6c2809a0c51e668d11794de0815b293fdb3a9060', + aiopsLogRateAnalysisEmbeddable: '6a2e9953140fa8e89f82c394d8bff3c1a5aefe66', }); }); }); diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts index 325526fc9a1e9..023ff4151835f 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts @@ -33,12 +33,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(types).to.eql({ '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', - aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', - ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147', - ml_anomaly_swimlane: 'a3517f3e53fb041e9cbb150477fb6ef0f731bd5f', - ml_single_metric_viewer: '8b9532b0a40dfdfa282e262949b82cc1a643147c', - aiopsPatternAnalysisEmbeddable: '6c2809a0c51e668d11794de0815b293fdb3a9060', - aiopsLogRateAnalysisEmbeddable: '6a2e9953140fa8e89f82c394d8bff3c1a5aefe66', }); }); }); From ff014424c9b1e4b25a69031436557fbf80341c73 Mon Sep 17 00:00:00 2001 From: christineweng Date: Tue, 14 Apr 2026 12:49:36 -0500 Subject: [PATCH 2/3] fix ftr --- .../registered_unified_attachment_types.ts | 45 ------------- ...e_basic.ts => registered_unified_basic.ts} | 36 ++++++++--- .../security_and_spaces/tests/basic/index.ts | 3 +- .../registered_persistable_state_trial.ts | 40 ------------ .../registered_unified_trial.ts | 64 +++++++++++++++++++ .../security_and_spaces/tests/trial/index.ts | 3 +- 6 files changed, 93 insertions(+), 98 deletions(-) delete mode 100644 x-pack/platform/test/cases_api_integration/security_and_spaces/tests/attachments_framework/registered_unified_attachment_types.ts rename x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/{registered_persistable_state_basic.ts => registered_unified_basic.ts} (53%) delete mode 100644 x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts create mode 100644 x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_unified_trial.ts diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/attachments_framework/registered_unified_attachment_types.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/attachments_framework/registered_unified_attachment_types.ts deleted file mode 100644 index 194d7cfdde293..0000000000000 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/attachments_framework/registered_unified_attachment_types.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; - -import type { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - - /** - * Internal unified types are registered in - * x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts - */ - describe('Unified attachment types', () => { - describe('check registered unified attachment types', () => { - const getRegisteredTypes = () => { - return supertest - .get('/api/cases_fixture/registered_unified_attachments') - .expect(200) - .then((response) => response.body); - }; - - it('should check changes on all registered unified attachment types', async () => { - const types = await getRegisteredTypes(); - - expect(types).to.eql({ - lens: '45d27f9672c86ca48baf24ef1b04d4802555aee2', - comment: '118a9989815489c24b81b160782015890ed2085e', - 'security.event': '0337735d3e57178e44b426e41e616aae57fd794d', - aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', - ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147', - ml_anomaly_swimlane: 'a3517f3e53fb041e9cbb150477fb6ef0f731bd5f', - ml_single_metric_viewer: '8b9532b0a40dfdfa282e262949b82cc1a643147c', - aiopsPatternAnalysisEmbeddable: '6c2809a0c51e668d11794de0815b293fdb3a9060', - aiopsLogRateAnalysisEmbeddable: '6a2e9953140fa8e89f82c394d8bff3c1a5aefe66', - }); - }); - }); - }); -}; diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_unified_basic.ts similarity index 53% rename from x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts rename to x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_unified_basic.ts index 023ff4151835f..524fa44c1d597 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_unified_basic.ts @@ -11,30 +11,48 @@ import type { FtrProviderContext } from '../../../../common/ftr_provider_context export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const getRegisteredPersistableStateTypes = () => { + return supertest + .get('/api/cases_fixture/registered_persistable_state_attachments') + .expect(200) + .then((response) => response.body); + }; + + const getRegisteredUnifiedTypes = () => { + return supertest + .get('/api/cases_fixture/registered_unified_attachments') + .expect(200) + .then((response) => response.body); + }; /** * Attachment types are being registered in * x-pack/platform/test/cases_api_integration/common/plugins/cases/server/plugin.ts */ - describe('Persistable state attachments', () => { + describe('Attachment registries', () => { // This test is intended to fail when new persistable state attachment types are registered. // To resolve, add the new persistable state attachment types ID to this list. This will trigger // a CODEOWNERS review by Response Ops. describe('check registered persistable state attachment types', () => { - const getRegisteredTypes = () => { - return supertest - .get('/api/cases_fixture/registered_persistable_state_attachments') - .expect(200) - .then((response) => response.body); - }; - it('should check changes on all registered persistable state attachment types', async () => { - const types = await getRegisteredTypes(); + const types = await getRegisteredPersistableStateTypes(); expect(types).to.eql({ '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', }); }); }); + + describe('check registered unified attachment types', () => { + it('should check changes on all registered unified attachment types for a basic license', async () => { + const types = await getRegisteredUnifiedTypes(); + + expect(types).to.eql({ + lens: '45d27f9672c86ca48baf24ef1b04d4802555aee2', + comment: '118a9989815489c24b81b160782015890ed2085e', + 'security.event': '0337735d3e57178e44b426e41e616aae57fd794d', + }); + }); + }); }); }; diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/index.ts index 1d8494efebd41..406e4e033fc89 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/basic/index.ts @@ -31,8 +31,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/assignees')); loadTestFile(require.resolve('./cases/push_case')); loadTestFile(require.resolve('./configure/get_connectors')); - loadTestFile(require.resolve('./attachments_framework/registered_persistable_state_basic')); - loadTestFile(require.resolve('../attachments_framework/registered_unified_attachment_types')); + loadTestFile(require.resolve('./attachments_framework/registered_unified_basic')); /** * Telemetry diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts deleted file mode 100644 index 023ff4151835f..0000000000000 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts +++ /dev/null @@ -1,40 +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 expect from '@kbn/expect'; - -import type { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - - /** - * Attachment types are being registered in - * x-pack/platform/test/cases_api_integration/common/plugins/cases/server/plugin.ts - */ - describe('Persistable state attachments', () => { - // This test is intended to fail when new persistable state attachment types are registered. - // To resolve, add the new persistable state attachment types ID to this list. This will trigger - // a CODEOWNERS review by Response Ops. - describe('check registered persistable state attachment types', () => { - const getRegisteredTypes = () => { - return supertest - .get('/api/cases_fixture/registered_persistable_state_attachments') - .expect(200) - .then((response) => response.body); - }; - - it('should check changes on all registered persistable state attachment types', async () => { - const types = await getRegisteredTypes(); - - expect(types).to.eql({ - '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', - }); - }); - }); - }); -}; diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_unified_trial.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_unified_trial.ts new file mode 100644 index 0000000000000..182ec76bd4020 --- /dev/null +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_unified_trial.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const getRegisteredPersistableStateTypes = () => { + return supertest + .get('/api/cases_fixture/registered_persistable_state_attachments') + .expect(200) + .then((response) => response.body); + }; + + const getRegisteredUnifiedTypes = () => { + return supertest + .get('/api/cases_fixture/registered_unified_attachments') + .expect(200) + .then((response) => response.body); + }; + + /** + * Attachment types are being registered in + * x-pack/platform/test/cases_api_integration/common/plugins/cases/server/plugin.ts + */ + describe('Attachment registries', () => { + // This test is intended to fail when new persistable state attachment types are registered. + // To resolve, add the new persistable state attachment types ID to this list. This will trigger + // a CODEOWNERS review by Response Ops. + describe('check registered persistable state attachment types', () => { + it('should check changes on all registered persistable state attachment types', async () => { + const types = await getRegisteredPersistableStateTypes(); + + expect(types).to.eql({ + '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', + }); + }); + }); + + describe('check registered unified attachment types', () => { + it('should check changes on all registered unified attachment types for a trial license', async () => { + const types = await getRegisteredUnifiedTypes(); + + expect(types).to.eql({ + lens: '45d27f9672c86ca48baf24ef1b04d4802555aee2', + comment: '118a9989815489c24b81b160782015890ed2085e', + 'security.event': '0337735d3e57178e44b426e41e616aae57fd794d', + 'aiops.change_point_chart': '2620ad738edfb370b0f9053c25ce0f40a1658ab7', + 'aiops.pattern_analysis': 'faba5f3bd68e94cbc848b27e5ecaf3d241033f28', + 'aiops.log_rate_analysis': '85dd3ad1fe168e5e70c8e647b29ce0b9e65e5df1', + 'ml.anomaly_charts': 'e52fc630b685fa5e8fa7f64c5a37c28304ee21ac', + 'ml.anomaly_swimlane': '6260e10758142f6ebe7e4d5e51b23d24b78abd66', + 'ml.single_metric_viewer': 'f011c0a8d142163e1d626ab78372bd9a8b5b444e', + }); + }); + }); + }); +}; diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/index.ts index d8d84ed00bc14..a54db67bf01d4 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/trial/index.ts @@ -32,8 +32,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/post_case')); loadTestFile(require.resolve('./cases/patch_case')); loadTestFile(require.resolve('./configure')); - loadTestFile(require.resolve('./attachments_framework/registered_persistable_state_trial')); - loadTestFile(require.resolve('../attachments_framework/registered_unified_attachment_types')); + loadTestFile(require.resolve('./attachments_framework/registered_unified_trial')); // sub privileges are only available with a license above basic loadTestFile(require.resolve('./delete_sub_privilege')); loadTestFile(require.resolve('./create_comment_sub_privilege.ts')); From 6bdb0ffcd61512464ccc5d62349f6667f6083b72 Mon Sep 17 00:00:00 2001 From: christineweng Date: Wed, 15 Apr 2026 14:24:34 -0500 Subject: [PATCH 3/3] fix api test --- .../server/client/attachments/validators.ts | 25 ++++++++---- .../apps/cases/group2/attachment_framework.ts | 39 +++++++++++-------- .../plugins/cases/public/plugin.ts | 7 ---- .../plugins/cases/server/plugin.ts | 2 - 4 files changed, 41 insertions(+), 32 deletions(-) diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/validators.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/validators.ts index a3bf29e495904..64a0a52454431 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/validators.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/validators.ts @@ -14,6 +14,8 @@ import { isUnifiedAttachmentRequest, isUnifiedReferenceAttachmentRequest, isUnifiedValueAttachmentRequest, + isPersistableType, + toUnifiedPersistableStateAttachmentType, } from '../../../common/utils/attachments'; import type { AttachmentRequest, AttachmentRequestV2 } from '../../../common/types/api'; import type { ExternalReferenceAttachmentTypeRegistry } from '../../attachment_framework/external_reference_registry'; @@ -24,10 +26,12 @@ export const validateLegacyRegisteredAttachments = ({ query, persistableStateAttachmentTypeRegistry, externalReferenceAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, }: { query: AttachmentRequest; persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry; externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; + unifiedAttachmentTypeRegistry: UnifiedAttachmentTypeRegistry; }) => { if ( isCommentRequestTypeExternalReference(query) && @@ -38,13 +42,19 @@ export const validateLegacyRegisteredAttachments = ({ ); } - if ( - isCommentRequestTypePersistableState(query) && - !persistableStateAttachmentTypeRegistry.has(query.persistableStateAttachmentTypeId) - ) { - throw Boom.badRequest( - `Attachment type ${query.persistableStateAttachmentTypeId} is not registered.` - ); + if (isCommentRequestTypePersistableState(query)) { + const typeId = query.persistableStateAttachmentTypeId; + + if (isPersistableType(typeId)) { + const unifiedTypeId = toUnifiedPersistableStateAttachmentType(typeId); + if (!unifiedAttachmentTypeRegistry.has(unifiedTypeId)) { + throw Boom.badRequest( + `Attachment type ${typeId} (unified: ${unifiedTypeId}) is not registered in unified attachment type registry.` + ); + } + } else if (!persistableStateAttachmentTypeRegistry.has(typeId)) { + throw Boom.badRequest(`Attachment type ${typeId} is not registered.`); + } } }; @@ -97,6 +107,7 @@ export const validateRegisteredAttachments = ({ query, persistableStateAttachmentTypeRegistry, externalReferenceAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, }); } else if (isUnifiedAttachmentRequest(query)) { validateUnifiedRegisteredAttachments({ diff --git a/x-pack/platform/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts b/x-pack/platform/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts index 02cd52311085a..5a43b723f4e77 100644 --- a/x-pack/platform/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts +++ b/x-pack/platform/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts @@ -16,6 +16,7 @@ import { ExternalReferenceStorageType, AttachmentType, } from '@kbn/cases-plugin/common/types/domain'; +import { LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common/constants'; import { expect } from 'expect'; import type { AttachmentRequest } from '@kbn/cases-plugin/common/types/api'; import { @@ -87,10 +88,13 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { return caseWithAttachment; }; - const validateAttachment = async (type: string, attachmentId?: string | null) => { - await testSubjects.existOrFail(`comment-${type}-.test`); + const validateAttachment = async ( + type: string, + attachmentId?: string | null, + attachmentTypeId = '.test' + ) => { + await testSubjects.existOrFail(`comment-${type}-${attachmentTypeId}`); await testSubjects.existOrFail(`copy-link-${attachmentId}`); - await testSubjects.existOrFail(`attachment-.test-${attachmentId}-chevronSingleRight`); }; /** @@ -117,7 +121,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); }); - describe('Persistable state attachments', () => { + describe('Lens attachments', () => { let caseWithAttachment: Case; let dataViewId = ''; @@ -128,8 +132,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const res = await createLogStashDataView(supertest); dataViewId = res.data_view.id; - const persistableStateAttachment = getPersistableStateAttachment(dataViewId); - caseWithAttachment = await createAttachmentAndNavigate(persistableStateAttachment); + const lensAttachment = getPersistableStateAttachment(dataViewId); + caseWithAttachment = await createAttachmentAndNavigate(lensAttachment); }); after(async () => { @@ -138,11 +142,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await esArchiver.unload('x-pack/platform/test/fixtures/es_archives/logstash_functional'); }); - it('renders a persistable attachment type correctly', async () => { + it('renders a lens attachment type correctly', async () => { const attachmentId = caseWithAttachment?.comments?.[0].id; - await validateAttachment(AttachmentType.persistableState, attachmentId); + await validateAttachment(LENS_ATTACHMENT_TYPE, attachmentId, LENS_ATTACHMENT_TYPE); await retry.waitFor( - 'persistable state to exist', + 'lens visualization to exist', async () => await find.existsByCssSelector('.lnsExpressionRenderer') ); }); @@ -164,7 +168,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); const externalReferenceAttachment = getExternalReferenceAttachment(); - const persistableStateAttachment = getPersistableStateAttachment(dataViewId); + const lensAttachment = getPersistableStateAttachment(dataViewId); await cases.api.createAttachment({ caseId: originalCase.id, @@ -173,7 +177,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.api.createAttachment({ caseId: originalCase.id, - params: persistableStateAttachment, + params: lensAttachment, }); await cases.navigation.navigateToApp(); @@ -197,13 +201,13 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const comments = userActions.filter((userAction) => userAction.type === 'comment'); const externalRefAttachmentId = comments[0].comment_id; - const persistableStateAttachmentId = comments[1].comment_id; + const lensAttachmentId = comments[1].comment_id; await validateAttachment(AttachmentType.externalReference, externalRefAttachmentId); - await validateAttachment(AttachmentType.persistableState, persistableStateAttachmentId); + await validateAttachment(LENS_ATTACHMENT_TYPE, lensAttachmentId, LENS_ATTACHMENT_TYPE); await testSubjects.existOrFail('test-attachment-content'); await retry.waitFor( - 'persistable state to exist', + 'lens visualization to exist', async () => await find.existsByCssSelector('.lnsExpressionRenderer') ); }); @@ -542,7 +546,10 @@ const getExternalReferenceAttachment = (): ExternalReferenceAttachmentPayload => const getPersistableStateAttachment = (dataViewId: string): PersistableStateAttachmentPayload => ({ type: AttachmentType.persistableState, - persistableStateAttachmentTypeId: '.test', - persistableStateAttachmentState: getLensState(dataViewId), + persistableStateAttachmentTypeId: '.lens', + persistableStateAttachmentState: { + attributes: getLensState(dataViewId), + timeRange: { from: 'now-15m', to: 'now', mode: 'relative' }, + }, owner: 'cases', }); diff --git a/x-pack/platform/test/functional_with_es_ssl/plugins/cases/public/plugin.ts b/x-pack/platform/test/functional_with_es_ssl/plugins/cases/public/plugin.ts index a538a3c623599..e13cdbeb5cdfb 100644 --- a/x-pack/platform/test/functional_with_es_ssl/plugins/cases/public/plugin.ts +++ b/x-pack/platform/test/functional_with_es_ssl/plugins/cases/public/plugin.ts @@ -9,7 +9,6 @@ import type { Plugin, CoreSetup, CoreStart, AppMountParameters } from '@kbn/core import type { CasesPublicSetup, CasesPublicStart } from '@kbn/cases-plugin/public/types'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; import { getExternalReferenceAttachmentRegular } from './attachments/external_reference'; -import { getPersistableStateAttachmentRegular } from './attachments/persistable_state'; export type Setup = void; export type Start = void; @@ -31,12 +30,6 @@ export class CasesFixturePlugin getExternalReferenceAttachmentRegular() ); - core.getStartServices().then(([_, depsStart]) => { - plugins.cases.attachmentFramework.registerPersistableState( - getPersistableStateAttachmentRegular(depsStart.lens.EmbeddableComponent) - ); - }); - core.application.register({ id: 'cases_fixture', title: 'Cases Fixture App', diff --git a/x-pack/platform/test/functional_with_es_ssl/plugins/cases/server/plugin.ts b/x-pack/platform/test/functional_with_es_ssl/plugins/cases/server/plugin.ts index 7b9d837569e80..937e41b108ccd 100644 --- a/x-pack/platform/test/functional_with_es_ssl/plugins/cases/server/plugin.ts +++ b/x-pack/platform/test/functional_with_es_ssl/plugins/cases/server/plugin.ts @@ -8,7 +8,6 @@ import type { CasesServerSetup } from '@kbn/cases-plugin/server/types'; import type { Plugin, CoreSetup } from '@kbn/core/server'; import { getExternalReferenceAttachment } from './attachments/external_reference'; -import { getPersistableStateAttachmentServer } from './attachments/persistable_state'; export interface CasesExamplePublicSetupDeps { cases: CasesServerSetup; @@ -17,7 +16,6 @@ export interface CasesExamplePublicSetupDeps { export class CasesFixturePlugin implements Plugin { public setup(core: CoreSetup, { cases }: CasesExamplePublicSetupDeps) { cases.attachmentFramework.registerExternalReference(getExternalReferenceAttachment()); - cases.attachmentFramework.registerPersistableState(getPersistableStateAttachmentServer()); } public start() {}