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 bc718fab700af..f7b86e83441c2 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/attachments.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/attachments.ts @@ -4,12 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER, GENERAL_CASES_OWNER } from './owners'; // ----------------Unified attachment types------------------------- export const COMMENT_ATTACHMENT_TYPE = 'comment'; export const SECURITY_EVENT_ATTACHMENT_TYPE = 'security.event'; +export const LENS_ATTACHMENT_TYPE = 'lens'; // ----------------Legacy attachment types------------------------- export const LEGACY_ACTIONS_TYPE = 'actions'; @@ -19,6 +19,8 @@ export const LEGACY_EXTERNAL_REFERENCE_TYPE = 'externalReference'; export const LEGACY_PERSISTABLE_STATE_TYPE = 'persistableState'; export const LEGACY_USER_TYPE = 'user'; +export const LEGACY_LENS_ATTACHMENT_TYPE = '.lens'; + export const LEGACY_ATTACHMENT_TYPES = new Set([ LEGACY_ACTIONS_TYPE, LEGACY_ALERT_TYPE, @@ -33,6 +35,17 @@ export const UNIFIED_ATTACHMENT_TYPES = new Set([ SECURITY_EVENT_ATTACHMENT_TYPE, ]); +export const PERSISTABLE_STATE_LEGACY_TO_UNIFIED_MAP: Record = { + [LEGACY_LENS_ATTACHMENT_TYPE]: LENS_ATTACHMENT_TYPE, +} as const; + +export const PERSISTABLE_STATE_UNIFIED_TO_LEGACY_MAP: Record = { + [LENS_ATTACHMENT_TYPE]: LEGACY_LENS_ATTACHMENT_TYPE, +} as const; + +export const PERSISTABLE_ATTACHMENT_TYPES = new Set( + Object.keys(PERSISTABLE_STATE_UNIFIED_TO_LEGACY_MAP) +); /** * Mapping from legacy attachment type names to unified names. */ @@ -54,6 +67,7 @@ export const UNIFIED_TO_LEGACY_MAP: Record = { export const MIGRATED_ATTACHMENT_TYPES = new Set([ COMMENT_ATTACHMENT_TYPE, SECURITY_EVENT_ATTACHMENT_TYPE, + ...PERSISTABLE_ATTACHMENT_TYPES, ]); export const OWNER_TO_PREFIX_MAP: Partial> = { diff --git a/x-pack/platform/plugins/shared/cases/common/constants/index.ts b/x-pack/platform/plugins/shared/cases/common/constants/index.ts index 67a4e6e7b7ba5..6c56ee7223fbb 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/index.ts @@ -12,7 +12,6 @@ export * from './files'; export * from './application'; export * from './observables'; export * from './attachments'; -export { LENS_ATTACHMENT_TYPE } from './visualizations'; /** * Cases connector limits. diff --git a/x-pack/platform/plugins/shared/cases/common/constants/visualizations.ts b/x-pack/platform/plugins/shared/cases/common/constants/visualizations.ts deleted file mode 100644 index 9af1681360d16..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/constants/visualizations.ts +++ /dev/null @@ -1,8 +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 LENS_ATTACHMENT_TYPE = '.lens'; diff --git a/x-pack/platform/plugins/shared/cases/common/utils/attachments/migration_utils.test.ts b/x-pack/platform/plugins/shared/cases/common/utils/attachments/migration_utils.test.ts index 761a4cd582c3b..1be1fc4851aa5 100644 --- a/x-pack/platform/plugins/shared/cases/common/utils/attachments/migration_utils.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/utils/attachments/migration_utils.test.ts @@ -5,9 +5,14 @@ * 2.0. */ +import { LEGACY_LENS_ATTACHMENT_TYPE, LENS_ATTACHMENT_TYPE } from '../../constants/attachments'; import { AttachmentType } from '../../types/domain'; import { SECURITY_SOLUTION_OWNER } from '../../constants'; -import { isMigratedAttachmentType, toUnifiedAttachmentType } from './migration_utils'; +import { + isMigratedAttachmentType, + isPersistableType, + toUnifiedAttachmentType, +} from './migration_utils'; const owner = SECURITY_SOLUTION_OWNER; @@ -23,6 +28,10 @@ describe('migration_utils', () => { expect(isMigratedAttachmentType('comment', owner)).toBe(true); expect(isMigratedAttachmentType('security.event', 'security')).toBe(true); }); + it('is true for legacy and unified Lens persistable subtype ids', () => { + expect(isMigratedAttachmentType(LEGACY_LENS_ATTACHMENT_TYPE, owner)).toBe(true); + expect(isMigratedAttachmentType(LENS_ATTACHMENT_TYPE, owner)).toBe(true); + }); it('is false for non-migrated attachment types', () => { expect(isMigratedAttachmentType(AttachmentType.alert, owner)).toBe(false); @@ -37,4 +46,15 @@ describe('migration_utils', () => { ); }); }); + + describe('isPersistableType', () => { + it('is true for Lens legacy and unified subtype ids', () => { + expect(isPersistableType(LEGACY_LENS_ATTACHMENT_TYPE)).toBe(true); + expect(isPersistableType(LENS_ATTACHMENT_TYPE)).toBe(true); + }); + + it('is false for unrelated persistable subtype ids', () => { + expect(isPersistableType('.test')).toBe(false); + }); + }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/utils/attachments/migration_utils.ts b/x-pack/platform/plugins/shared/cases/common/utils/attachments/migration_utils.ts index 9abd20f3bc028..7d5ea62978542 100644 --- a/x-pack/platform/plugins/shared/cases/common/utils/attachments/migration_utils.ts +++ b/x-pack/platform/plugins/shared/cases/common/utils/attachments/migration_utils.ts @@ -8,19 +8,28 @@ import { LEGACY_TO_UNIFIED_MAP, MIGRATED_ATTACHMENT_TYPES, + PERSISTABLE_STATE_LEGACY_TO_UNIFIED_MAP, + PERSISTABLE_STATE_UNIFIED_TO_LEGACY_MAP, + PERSISTABLE_ATTACHMENT_TYPES, UNIFIED_TO_LEGACY_MAP, OWNER_TO_PREFIX_MAP, LEGACY_EVENT_TYPE, } from '../../constants/attachments'; export const isMigratedAttachmentType = (type: string, owner: string): boolean => { - return MIGRATED_ATTACHMENT_TYPES.has(toUnifiedAttachmentType(type, owner)); + return ( + MIGRATED_ATTACHMENT_TYPES.has(toUnifiedAttachmentType(type, owner)) || + MIGRATED_ATTACHMENT_TYPES.has(toUnifiedPersistableStateAttachmentType(type)) + ); }; export const toLegacyAttachmentType = (type?: string): string | undefined => { if (typeof type !== 'string') { return undefined; } + if (type in PERSISTABLE_STATE_UNIFIED_TO_LEGACY_MAP) { + return toLegacyPersistableStateAttachmentType(type); + } return UNIFIED_TO_LEGACY_MAP[type] ?? type; }; @@ -34,3 +43,18 @@ export const toUnifiedAttachmentType = (type: string, owner: string): string => } return LEGACY_TO_UNIFIED_MAP[type] ?? type; }; + +/** + * True when the persistable-state subtype id (legacy `.lens` or unified `lens`) is one + * that this stack migrates to unified attachment attributes (currently Lens only). + */ +export const isPersistableType = (type: string): boolean => + PERSISTABLE_ATTACHMENT_TYPES.has(toUnifiedPersistableStateAttachmentType(type)); + +export const toUnifiedPersistableStateAttachmentType = (type: string): string => { + return PERSISTABLE_STATE_LEGACY_TO_UNIFIED_MAP[type] ?? type; +}; + +export const toLegacyPersistableStateAttachmentType = (type: string): string => { + return PERSISTABLE_STATE_UNIFIED_TO_LEGACY_MAP[type] ?? type; +}; diff --git a/x-pack/platform/plugins/shared/cases/public/api/utils.ts b/x-pack/platform/plugins/shared/cases/public/api/utils.ts index 123c1714faa83..1c6af86edb8ed 100644 --- a/x-pack/platform/plugins/shared/cases/public/api/utils.ts +++ b/x-pack/platform/plugins/shared/cases/public/api/utils.ts @@ -24,6 +24,7 @@ import type { import { isCommentRequestTypeExternalReference, isCommentRequestTypePersistableState, + isUnifiedAttachmentRequest, } from '../../common/utils/attachments'; import { isCommentUserAction } from '../../common/utils/user_actions'; import type { @@ -103,6 +104,10 @@ export const convertAttachmentToCamelCase = (attachment: AttachmentRequestV2): A return convertAttachmentToCamelExceptProperty(attachment, 'persistableStateAttachmentState'); } + if (isUnifiedAttachmentRequest(attachment)) { + return convertAttachmentToCamelExceptProperty(attachment, 'data'); + } + return convertToCamelCase(attachment); }; diff --git a/x-pack/platform/plugins/shared/cases/public/components/attachments/index.ts b/x-pack/platform/plugins/shared/cases/public/components/attachments/index.ts index b465b80d663c6..f7d8e0da5815f 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/attachments/index.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/attachments/index.ts @@ -6,18 +6,16 @@ */ import type { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; -import type { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; import type { UnifiedAttachmentTypeRegistry } from '../../client/attachment_framework/unified_attachment_registry'; import { getCommentAttachmentType } from './comment'; import { getFileType } from './file/file_type'; -import { getVisualizationAttachmentType } from './lens/attachment'; +import { getVisualizationAttachmentType } from './lens'; export const registerInternalAttachments = ( externalRefRegistry: ExternalReferenceAttachmentTypeRegistry, - persistableStateRegistry: PersistableStateAttachmentTypeRegistry, unifiedRegistry: UnifiedAttachmentTypeRegistry ) => { externalRefRegistry.register(getFileType()); - persistableStateRegistry.register(getVisualizationAttachmentType()); + unifiedRegistry.register(getVisualizationAttachmentType()); unifiedRegistry.register(getCommentAttachmentType()); }; diff --git a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/open_modal.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/open_modal.test.tsx index de8c24aa06b9a..f84dd49c0a437 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/open_modal.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/open_modal.test.tsx @@ -20,6 +20,7 @@ import { useKibana } from '../../../../common/lib/kibana'; import { waitFor } from '@testing-library/react'; import { openModal } from './open_modal'; import type { CasesActionContextProps } from './types'; +import { LENS_ATTACHMENT_TYPE } from '../../../../../common/constants/attachments'; const element = document.createElement('div'); document.body.appendChild(element); @@ -99,15 +100,16 @@ describe('openModal', () => { const res = getAttachments(); expect(res).toEqual([ { - persistableStateAttachmentState: { - attributes: mockLensAttributes, - timeRange: { - from: '2023-12-31T00:00:00.000Z', - to: '2024-01-01T00:00:00.000Z', + type: LENS_ATTACHMENT_TYPE, + data: { + state: { + attributes: mockLensAttributes, + timeRange: { + from: '2023-12-31T00:00:00.000Z', + to: '2024-01-01T00:00:00.000Z', + }, }, }, - persistableStateAttachmentTypeId: '.lens', - type: 'persistableState', }, ]); }); @@ -188,15 +190,16 @@ describe('openModal', () => { const res = getAttachments(); expect(res).toEqual([ { - persistableStateAttachmentState: { - attributes: mockLensAttributes, - timeRange: { - from: '2024-01-09T00:00:00.000Z', - to: '2024-01-10T00:00:00.000Z', + type: LENS_ATTACHMENT_TYPE, + data: { + state: { + attributes: mockLensAttributes, + timeRange: { + from: '2024-01-09T00:00:00.000Z', + to: '2024-01-10T00:00:00.000Z', + }, }, }, - persistableStateAttachmentTypeId: '.lens', - type: 'persistableState', }, ]); }); @@ -218,15 +221,16 @@ describe('openModal', () => { expect(res).toEqual([ { - persistableStateAttachmentState: { - attributes: mockLensAttributes, - timeRange: { - from: '2023-12-01T00:00:00.000Z', - to: '2024-01-01T00:00:00.000Z', + type: LENS_ATTACHMENT_TYPE, + data: { + state: { + attributes: mockLensAttributes, + timeRange: { + from: '2023-12-01T00:00:00.000Z', + to: '2024-01-01T00:00:00.000Z', + }, }, }, - persistableStateAttachmentTypeId: '.lens', - type: 'persistableState', }, ]); }); diff --git a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/utils.test.ts b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/utils.test.ts index d643a668553d3..0fda37fab9275 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/utils.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/utils.test.ts @@ -15,13 +15,14 @@ describe('utils', () => { // @ts-expect-error: extra attributes are not needed expect(getLensCaseAttachment(embeddable)).toMatchInlineSnapshot(` Object { - "persistableStateAttachmentState": Object { - "attributes": Object {}, - "metadata": undefined, - "timeRange": Object {}, + "data": Object { + "state": Object { + "attributes": Object {}, + "metadata": undefined, + "timeRange": Object {}, + }, }, - "persistableStateAttachmentTypeId": ".lens", - "type": "persistableState", + "type": "lens", } `); }); diff --git a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/utils.ts b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/utils.ts index 7f0bab409712b..0c42c5706f596 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/utils.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/actions/utils.ts @@ -5,12 +5,11 @@ * 2.0. */ import type { LensSavedObjectAttributes } from '@kbn/lens-plugin/public'; -import { LENS_ATTACHMENT_TYPE } from '../../../../../common/constants/visualizations'; -import type { PersistableStateAttachmentPayload } from '../../../../../common/types/domain'; -import { AttachmentType } from '../../../../../common/types/domain'; +import { LENS_ATTACHMENT_TYPE } from '../../../../../common/constants/attachments'; +import type { UnifiedValueAttachmentPayload } from '../../../../../common/types/domain'; import type { LensProps } from '../types'; -type PersistableStateAttachmentWithoutOwner = Omit; +type UnifiedValueAttachmentWithoutOwner = Omit; export const getLensCaseAttachment = ({ timeRange, @@ -20,9 +19,10 @@ export const getLensCaseAttachment = ({ timeRange: LensProps['timeRange']; attributes: LensSavedObjectAttributes; metadata?: LensProps['metadata']; -}): PersistableStateAttachmentWithoutOwner => +}): UnifiedValueAttachmentWithoutOwner => ({ - persistableStateAttachmentState: { attributes, timeRange, metadata }, - persistableStateAttachmentTypeId: LENS_ATTACHMENT_TYPE, - type: AttachmentType.persistableState, - } as unknown as PersistableStateAttachmentWithoutOwner); + type: LENS_ATTACHMENT_TYPE, + data: { + state: { attributes, timeRange, metadata }, + }, + } as unknown as UnifiedValueAttachmentWithoutOwner); diff --git a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/attachment.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/index.test.tsx similarity index 85% rename from x-pack/platform/plugins/shared/cases/public/components/attachments/lens/attachment.test.tsx rename to x-pack/platform/plugins/shared/cases/public/components/attachments/lens/index.test.tsx index 95495842d2103..c650af96beae0 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/attachment.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/index.test.tsx @@ -8,11 +8,11 @@ import React, { Suspense } from 'react'; import { screen, waitFor } from '@testing-library/react'; import { LENS_ATTACHMENT_TYPE } from '../../../../common'; -import type { PersistableStateAttachmentViewProps } from '../../../client/attachment_framework/types'; +import type { UnifiedValueAttachmentViewProps } from '../../../client/attachment_framework/types'; import { AttachmentActionType } from '../../../client/attachment_framework/types'; import { basicCase } from '../../../containers/mock'; -import { getVisualizationAttachmentType } from './attachment'; +import { getVisualizationAttachmentType } from '.'; import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; import { renderWithTestingProviders } from '../../../common/mock'; @@ -21,15 +21,27 @@ describe('getVisualizationAttachmentType', () => { .fn() .mockReturnValue(
); - const attachmentViewProps: PersistableStateAttachmentViewProps = { - persistableStateAttachmentTypeId: LENS_ATTACHMENT_TYPE, - persistableStateAttachmentState: { - attributes: { state: { query: {} } }, - timeRange: {}, + const attachmentViewProps: UnifiedValueAttachmentViewProps = { + type: LENS_ATTACHMENT_TYPE, + data: { + state: { + attributes: { state: { query: {} } }, + timeRange: {}, + }, }, + owner: 'securitySolution', + createdBy: { username: 'elastic', full_name: null, email: null, profile_uid: null }, + version: '1', savedObjectId: 'test', caseData: { title: basicCase.title, id: basicCase.id }, - }; + rowContext: { + appId: 'cases', + manageMarkdownEditIds: [], + selectedOutlineCommentId: '', + loadingCommentIds: [], + euiTheme: {} as never, + }, + } as unknown as UnifiedValueAttachmentViewProps; beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/attachment.tsx b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/index.tsx similarity index 76% rename from x-pack/platform/plugins/shared/cases/public/components/attachments/lens/attachment.tsx rename to x-pack/platform/plugins/shared/cases/public/components/attachments/lens/index.tsx index 65992b927f6a5..f96551ed83b3c 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/attachment.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/index.tsx @@ -8,12 +8,12 @@ import React from 'react'; import deepEqual from 'fast-deep-equal'; -import { LENS_ATTACHMENT_TYPE } from '../../../../common/constants/visualizations'; +import { LENS_ATTACHMENT_TYPE } from '../../../../common/constants'; import * as i18n from './translations'; import type { - PersistableStateAttachmentType, - PersistableStateAttachmentViewProps, + UnifiedValueAttachmentType, + UnifiedValueAttachmentViewProps, } from '../../../client/attachment_framework/types'; import { AttachmentActionType } from '../../../client/attachment_framework/types'; import type { LensProps } from './types'; @@ -40,14 +40,11 @@ const getVisualizationAttachmentActions = (savedObjectId: string, props: LensPro ]; const LensAttachment = React.memo( - (props: PersistableStateAttachmentViewProps) => { - const { attributes, timeRange, metadata } = - props.persistableStateAttachmentState as unknown as LensProps; - + (props: UnifiedValueAttachmentViewProps) => { + const { attributes, timeRange, metadata } = props.data.state as unknown as LensProps; return ; }, - (prevProps, nextProps) => - deepEqual(prevProps.persistableStateAttachmentState, nextProps.persistableStateAttachmentState) + (prevProps, nextProps) => deepEqual(prevProps.data.state, nextProps.data.state) ); LensAttachment.displayName = 'LensAttachment'; @@ -60,10 +57,10 @@ const LensAttachmentRendererLazyComponent = React.lazy(async () => { const getVisualizationAttachmentViewObject = ({ savedObjectId, - persistableStateAttachmentState, -}: PersistableStateAttachmentViewProps) => { + data, +}: UnifiedValueAttachmentViewProps) => { const { attributes: lensAttributes, timeRange: lensTimeRange } = - persistableStateAttachmentState as unknown as LensProps; + data.state as unknown as LensProps; return { event: i18n.ADDED_VISUALIZATION, @@ -78,10 +75,10 @@ const getVisualizationAttachmentViewObject = ({ }; }; -export const getVisualizationAttachmentType = (): PersistableStateAttachmentType => ({ +export const getVisualizationAttachmentType = (): UnifiedValueAttachmentType => ({ id: LENS_ATTACHMENT_TYPE, icon: 'document', - displayName: 'Visualizations', + displayName: i18n.VISUALIZATIONS, getAttachmentViewObject: getVisualizationAttachmentViewObject, getAttachmentRemovalObject: () => ({ event: i18n.REMOVED_VISUALIZATION }), }); diff --git a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/translations.tsx b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/translations.tsx index 0d450914645e7..d00ac977014e9 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/translations.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/attachments/lens/translations.tsx @@ -7,6 +7,10 @@ import { i18n } from '@kbn/i18n'; +export const VISUALIZATIONS = i18n.translate('xpack.cases.caseView.visualizations.displayName', { + defaultMessage: 'Visualizations', +}); + export const ADDED_VISUALIZATION = i18n.translate( 'xpack.cases.caseView.visualizations.addedVisualization', { diff --git a/x-pack/platform/plugins/shared/cases/public/plugin.ts b/x-pack/platform/plugins/shared/cases/public/plugin.ts index 15ce8723433fa..e31fd0e0f6e9e 100644 --- a/x-pack/platform/plugins/shared/cases/public/plugin.ts +++ b/x-pack/platform/plugins/shared/cases/public/plugin.ts @@ -78,7 +78,6 @@ export class CasesUiPlugin registerInternalAttachments( externalReferenceAttachmentTypeRegistry, - persistableStateAttachmentTypeRegistry, unifiedAttachmentTypeRegistry ); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/utils.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/utils.ts index f496bab9bb065..7f540e4722205 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/utils.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/utils.ts @@ -90,6 +90,8 @@ export const getLatestPushInfo = ( return null; }; +// Only used for comment and action attachments. +// TODO: https://github.com/elastic/kibana/issues/262574 const getCommentContent = (comment: AttachmentV2): string => { if (isLegacyAttachmentRequest(comment)) { if (comment.type === AttachmentType.user) { diff --git a/x-pack/platform/plugins/shared/cases/server/common/attachments/index.test.ts b/x-pack/platform/plugins/shared/cases/server/common/attachments/index.test.ts index af40fd6fadd5e..d81ff1d6f9423 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/attachments/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/attachments/index.test.ts @@ -6,7 +6,11 @@ */ import { AttachmentType } from '../../../common/types/domain'; -import { COMMENT_ATTACHMENT_TYPE } from '../../../common/constants/attachments'; +import { + COMMENT_ATTACHMENT_TYPE, + LEGACY_LENS_ATTACHMENT_TYPE, + LENS_ATTACHMENT_TYPE, +} from '../../../common/constants/attachments'; import { getAttachmentTypeFromAttributes, getAttachmentTypeTransformers } from '.'; import { commentAttachmentTransformer } from './comment'; @@ -42,6 +46,15 @@ describe('common/attachments', () => { ); }); + it('returns persistableStateAttachmentTypeId for persistable state attachments', () => { + expect( + getAttachmentTypeFromAttributes({ + type: AttachmentType.persistableState, + persistableStateAttachmentTypeId: LEGACY_LENS_ATTACHMENT_TYPE, + }) + ).toBe(LEGACY_LENS_ATTACHMENT_TYPE); + }); + it('throws when attributes have no recognizable attachment type', () => { expect(() => getAttachmentTypeFromAttributes({ foo: 'bar' })).toThrow( 'Invalid attributes: missing attachment type' @@ -81,5 +94,18 @@ describe('common/attachments', () => { expect(transformer).not.toBe(commentAttachmentTransformer); expect(transformer.isType({ type: AttachmentType.alert } as never)).toBe(false); }); + + it('returns configured persistable state transformer for known visualization types', () => { + const lensTransformer = getAttachmentTypeTransformers(LENS_ATTACHMENT_TYPE, owner); + expect(lensTransformer).not.toBe(commentAttachmentTransformer); + expect( + lensTransformer.isLegacyType({ + type: AttachmentType.persistableState, + persistableStateAttachmentTypeId: LEGACY_LENS_ATTACHMENT_TYPE, + persistableStateAttachmentState: {}, + owner: 'securitySolution', + }) + ).toBe(true); + }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/common/attachments/index.ts b/x-pack/platform/plugins/shared/cases/server/common/attachments/index.ts index b8b4ee6d2b1cf..e0e1ff93db313 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/attachments/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/attachments/index.ts @@ -5,17 +5,23 @@ * 2.0. */ -import { toUnifiedAttachmentType } from '../../../common/utils/attachments'; import { COMMENT_ATTACHMENT_TYPE, SECURITY_EVENT_ATTACHMENT_TYPE, + PERSISTABLE_ATTACHMENT_TYPES, } from '../../../common/constants/attachments'; +import { + toUnifiedAttachmentType, + toUnifiedPersistableStateAttachmentType, +} from '../../../common/utils/attachments'; +import { AttachmentType } from '../../../common/types/domain'; import type { AttachmentPersistedAttributes, UnifiedAttachmentAttributes, } from '../types/attachments_v2'; import { passThroughTransformer, type AttachmentTypeTransformer } from './base'; import { commentAttachmentTransformer } from './comment'; +import { persistableStateAttachmentTransformer } from './persistable_state'; import { eventAttachmentTransformer } from './event'; export { getCommentContentFromUnifiedPayload, commentAttachmentTransformer } from './comment'; @@ -25,36 +31,47 @@ export { } from './saved_object_type'; /** - * Returns the attachment type string from attributes (unified or legacy shape). - * Used to select the correct transformer. + * Returns a routing key for transformer selection (not necessarily a normalized unified type). + * For legacy `persistableState` attachments this is `persistableStateAttachmentTypeId` (e.g. `.lens`); + * for all other shapes it is the top-level `type` (e.g. `user`, `alert`, unified `lens`). + * Use `toUnifiedAttachmentType` / `toUnifiedPersistableStateAttachmentType` from migration utils to normalize. * @throws Error if attributes is null or not an object */ export function getAttachmentTypeFromAttributes(attributes: unknown): string { if (attributes === null || typeof attributes !== 'object') { throw new Error('Invalid attributes: expected non-null object'); } - const { type } = attributes as Record; + const { type, persistableStateAttachmentTypeId } = attributes as Record; if (typeof type !== 'string') { throw new Error('Invalid attributes: missing attachment type'); } + if ( + type === AttachmentType.persistableState && + typeof persistableStateAttachmentTypeId === 'string' + ) { + return persistableStateAttachmentTypeId; + } return type; } /** - * Returns the persisted transformer for the given attachment type. - * Use getAttachmentTypeFromAttributes(attributes) to derive type from decoded attributes. - * For comment/user types returns the comment transformer; for all other types returns a - * pass-through transformer (identity for old <-> new schema). + * Returns the persisted transformer for the routing key from {@link getAttachmentTypeFromAttributes}. + * For comment/user types returns the comment transformer; for migrated persistable + * types (e.g. Lens) returns the persistable-state transformer; otherwise pass-through. */ export function getAttachmentTypeTransformers( type: string, owner: string ): AttachmentTypeTransformer { const normalizedType = toUnifiedAttachmentType(type, owner); + const normalizedPersistableType = toUnifiedPersistableStateAttachmentType(type); if (normalizedType === COMMENT_ATTACHMENT_TYPE || normalizedType === 'comment') { return commentAttachmentTransformer; } + if (PERSISTABLE_ATTACHMENT_TYPES.has(normalizedPersistableType)) { + return persistableStateAttachmentTransformer; + } if (normalizedType === SECURITY_EVENT_ATTACHMENT_TYPE) { return eventAttachmentTransformer; } diff --git a/x-pack/platform/plugins/shared/cases/server/common/attachments/persistable_state.test.ts b/x-pack/platform/plugins/shared/cases/server/common/attachments/persistable_state.test.ts new file mode 100644 index 0000000000000..eacaa23ab3d44 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/server/common/attachments/persistable_state.test.ts @@ -0,0 +1,94 @@ +/* + * 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 { AttachmentType } from '../../../common/types/domain'; +import { + LEGACY_LENS_ATTACHMENT_TYPE, + LENS_ATTACHMENT_TYPE, +} from '../../../common/constants/attachments'; +import { persistableStateAttachmentTransformer } from './persistable_state'; +import type { AttachmentRequestV2 } from '../../../common/types/api'; + +describe('persistableStateAttachmentTransformer', () => { + const transformer = persistableStateAttachmentTransformer; + + it('converts legacy persistable state attributes to unified schema', () => { + const legacy = { + type: AttachmentType.persistableState, + owner: 'securitySolution', + persistableStateAttachmentTypeId: LEGACY_LENS_ATTACHMENT_TYPE, + persistableStateAttachmentState: { attributes: { title: 'Lens title' } }, + created_at: '2026-01-01T00:00:00.000Z', + created_by: { username: 'elastic', full_name: null, email: null, profile_uid: 'abc' }, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + }; + + const unified = transformer.toUnifiedSchema(legacy); + + expect(unified.type).toBe(LENS_ATTACHMENT_TYPE); + expect(unified.data).toEqual({ state: legacy.persistableStateAttachmentState }); + expect(unified.owner).toBe(legacy.owner); + }); + + it('accepts legacy persistable attachments stored with unified type id', () => { + const legacyWithUnifiedId = { + type: AttachmentType.persistableState, + owner: 'securitySolution', + persistableStateAttachmentTypeId: LENS_ATTACHMENT_TYPE, + persistableStateAttachmentState: { attributes: { title: 'Lens title' } }, + created_at: '2026-01-01T00:00:00.000Z', + created_by: { username: 'elastic', full_name: null, email: null, profile_uid: 'abc' }, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + }; + + const unified = transformer.toUnifiedSchema(legacyWithUnifiedId); + expect(unified.type).toBe(LENS_ATTACHMENT_TYPE); + expect(unified.data).toEqual({ state: legacyWithUnifiedId.persistableStateAttachmentState }); + }); + + it('converts unified schema back to legacy persistable state attributes', () => { + const unified = { + type: LENS_ATTACHMENT_TYPE, + owner: 'securitySolution', + data: { + state: { + attributes: { title: 'Lens title' }, + }, + }, + created_at: '2026-01-01T00:00:00.000Z', + created_by: { username: 'elastic', full_name: null, email: null, profile_uid: 'abc' }, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + }; + + const legacy = transformer.toLegacySchema(unified); + + expect(legacy.type).toBe(AttachmentType.persistableState); + expect(legacy.persistableStateAttachmentTypeId).toBe(LEGACY_LENS_ATTACHMENT_TYPE); + expect(legacy.persistableStateAttachmentState).toEqual(unified.data.state); + }); + + it('does not treat non-Lens legacy persistable state as this transformer legacy payload', () => { + const legacy = { + type: AttachmentType.persistableState, + owner: 'securitySolution', + persistableStateAttachmentTypeId: '.test', + persistableStateAttachmentState: {}, + }; + + expect(transformer.isLegacyPayload(legacy as AttachmentRequestV2)).toBe(false); + expect(transformer.isLegacyType(legacy)).toBe(false); + }); +}); diff --git a/x-pack/platform/plugins/shared/cases/server/common/attachments/persistable_state.ts b/x-pack/platform/plugins/shared/cases/server/common/attachments/persistable_state.ts new file mode 100644 index 0000000000000..ae310e409dbbf --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/server/common/attachments/persistable_state.ts @@ -0,0 +1,248 @@ +/* + * 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'; +import type { AttachmentRequest, AttachmentRequestV2 } from '../../../common/types/api'; +import type { + AttachmentAttributesV2, + UnifiedAttachmentPayload, +} from '../../../common/types/domain/attachment/v2'; +import { AttachmentType } from '../../../common/types/domain'; +import { + isPersistableType, + toLegacyPersistableStateAttachmentType, + toUnifiedPersistableStateAttachmentType, +} from '../../../common/utils/attachments'; +import type { + AttachmentPersistedAttributes, + UnifiedAttachmentAttributes, +} from '../types/attachments_v2'; +import type { AttachmentTypeTransformer } from './base'; + +const isRecord = (value: unknown): value is Record => isPlainObject(value); + +const isLegacyPersistableState = ( + value: unknown +): value is AttachmentPersistedAttributes & { + type: AttachmentType.persistableState; + persistableStateAttachmentTypeId: string; +} => + isRecord(value) && + value.type === AttachmentType.persistableState && + typeof value.owner === 'string' && + typeof value.persistableStateAttachmentTypeId === 'string'; + +const getStateFromLegacyAttachment = ( + attachment: AttachmentPersistedAttributes & { + type: AttachmentType.persistableState; + persistableStateAttachmentTypeId: string; + } +): AttachmentPersistedAttributes['persistableStateAttachmentState'] => { + if (isRecord(attachment.persistableStateAttachmentState)) { + return attachment.persistableStateAttachmentState; + } + + return {}; +}; + +const isUnifiedValueAttachmentForType = ( + value: unknown, + typeId: string +): value is UnifiedAttachmentAttributes & { data: { state?: Record } } => + isRecord(value) && + value.type === typeId && + typeof value.owner === 'string' && + isRecord(value.data); + +const getStateFromUnifiedData = ( + data: Record +): AttachmentPersistedAttributes['persistableStateAttachmentState'] => { + const state = data.state; + if (isRecord(state)) { + return state as AttachmentPersistedAttributes['persistableStateAttachmentState']; + } + + return data as AttachmentPersistedAttributes['persistableStateAttachmentState']; +}; + +// ---- Request layer (API / DTO) ---- +export function isLegacyPayloadPersistableStateAttachment( + attachment: AttachmentRequestV2 +): boolean { + return ( + isLegacyPersistableState(attachment) && + isPersistableType(attachment.persistableStateAttachmentTypeId) + ); +} + +export function isUnifiedPayloadPersistableStateAttachment( + attachment: AttachmentRequestV2 +): boolean { + return ( + isRecord(attachment) && + typeof attachment.type === 'string' && + isPersistableType(attachment.type) && + isUnifiedValueAttachmentForType(attachment, attachment.type) + ); +} + +export function toUnifiedPayloadPersistableStateAttachment( + attachment: AttachmentRequestV2 +): UnifiedAttachmentPayload { + if ( + isLegacyPersistableState(attachment) && + isPersistableType(attachment.persistableStateAttachmentTypeId) + ) { + const unifiedTypeId = toUnifiedPersistableStateAttachmentType( + attachment.persistableStateAttachmentTypeId + ); + const legacyState = getStateFromLegacyAttachment(attachment); + return { + type: unifiedTypeId, + owner: attachment.owner, + data: { state: legacyState }, + } as UnifiedAttachmentPayload; + } + + return attachment as unknown as UnifiedAttachmentPayload; +} + +export function toLegacyPayloadPersistableStateAttachment( + unifiedPayload: AttachmentRequestV2 +): AttachmentRequest { + if (isRecord(unifiedPayload) && typeof unifiedPayload.type === 'string') { + if ( + !isPersistableType(unifiedPayload.type) || + !isUnifiedValueAttachmentForType(unifiedPayload, unifiedPayload.type) + ) { + return unifiedPayload as AttachmentRequest; + } + const legacyTypeId = toLegacyPersistableStateAttachmentType(unifiedPayload.type); + const legacyState = getStateFromUnifiedData(unifiedPayload.data) ?? {}; + return { + type: AttachmentType.persistableState, + owner: unifiedPayload.owner, + persistableStateAttachmentTypeId: legacyTypeId, + persistableStateAttachmentState: legacyState, + }; + } + + return unifiedPayload as AttachmentRequest; +} + +// ---- Persisted layer (SO attributes) ---- +function isNewSchema(attributes: AttachmentAttributesV2): boolean { + return ( + isRecord(attributes) && + typeof attributes.type === 'string' && + isPersistableType(attributes.type) && + isUnifiedValueAttachmentForType(attributes, attributes.type) + ); +} + +function isOldSchema(attributes: AttachmentAttributesV2): boolean { + return ( + isLegacyPersistableState(attributes) && + isPersistableType(attributes.persistableStateAttachmentTypeId) + ); +} + +/** + * Transformer for migrated persistable visualization attachments (e.g. Lens): legacy + * `persistableState` wrapper <-> unified value shape (`type` + `data.state`). + */ +export const persistableStateAttachmentTransformer: AttachmentTypeTransformer< + AttachmentPersistedAttributes, + UnifiedAttachmentAttributes +> = { + toUnifiedSchema(attributes: unknown): UnifiedAttachmentAttributes { + const attrs = attributes as AttachmentAttributesV2; + if (isNewSchema(attrs)) { + return attrs as UnifiedAttachmentAttributes; + } + if (isOldSchema(attrs)) { + const oldAttrs = attrs as AttachmentPersistedAttributes & { + type: AttachmentType.persistableState; + persistableStateAttachmentTypeId: string; + }; + const unifiedTypeId = toUnifiedPersistableStateAttachmentType( + oldAttrs.persistableStateAttachmentTypeId + ); + const legacyState = getStateFromLegacyAttachment(oldAttrs); + return { + type: unifiedTypeId, + owner: oldAttrs.owner, + data: { state: legacyState }, + created_at: oldAttrs.created_at, + created_by: oldAttrs.created_by, + pushed_at: oldAttrs.pushed_at ?? null, + pushed_by: oldAttrs.pushed_by ?? null, + updated_at: oldAttrs.updated_at ?? null, + updated_by: oldAttrs.updated_by ?? null, + } as UnifiedAttachmentAttributes; + } + return attrs as UnifiedAttachmentAttributes; + }, + + toLegacySchema(attributes: unknown): AttachmentPersistedAttributes { + const attrs = attributes as AttachmentPersistedAttributes | UnifiedAttachmentAttributes; + const attrsAsCombined = attrs as AttachmentAttributesV2; + + if (isOldSchema(attrsAsCombined)) { + return attrs as unknown as AttachmentPersistedAttributes; + } + + if (isNewSchema(attrsAsCombined)) { + const newAttrs = attrs as UnifiedAttachmentAttributes; + const legacyTypeId = toLegacyPersistableStateAttachmentType(newAttrs.type); + const legacyState = getStateFromUnifiedData(newAttrs.data as Record) ?? {}; + return { + type: AttachmentType.persistableState, + owner: newAttrs.owner, + persistableStateAttachmentTypeId: legacyTypeId, + persistableStateAttachmentState: legacyState, + created_at: newAttrs.created_at, + created_by: newAttrs.created_by, + pushed_at: newAttrs.pushed_at ?? null, + pushed_by: newAttrs.pushed_by ?? null, + updated_at: newAttrs.updated_at ?? null, + updated_by: newAttrs.updated_by ?? null, + }; + } + + return attrs as AttachmentPersistedAttributes; + }, + + isType(attributes: AttachmentAttributesV2): boolean { + return isOldSchema(attributes) || isNewSchema(attributes); + }, + + isUnifiedType(attributes: AttachmentAttributesV2): boolean { + return isNewSchema(attributes); + }, + + isLegacyType(attributes: AttachmentAttributesV2): boolean { + return isOldSchema(attributes); + }, + + // --- Request payload (API layer) --- + isLegacyPayload(attachment: AttachmentRequestV2): boolean { + return isLegacyPayloadPersistableStateAttachment(attachment); + }, + + isUnifiedPayload(attachment: AttachmentRequestV2): boolean { + return isUnifiedPayloadPersistableStateAttachment(attachment); + }, + + toUnifiedPayload(attachment: AttachmentRequestV2): UnifiedAttachmentPayload { + return toUnifiedPayloadPersistableStateAttachment(attachment); + }, + + toLegacyPayload(attachment: AttachmentRequestV2): AttachmentRequest { + return toLegacyPayloadPersistableStateAttachment(attachment); + }, +}; diff --git a/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts b/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts index cc9c93f301119..97d365423d0b8 100644 --- a/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/internal_attachments/index.ts @@ -6,6 +6,7 @@ */ import { badRequest } from '@hapi/boom'; +import * as rt from 'io-ts'; import { FileAttachmentMetadataRt } from '../../common/types/domain'; import { FILE_ATTACHMENT_TYPE, LENS_ATTACHMENT_TYPE } from '../../common/constants'; @@ -14,6 +15,7 @@ import type { ExternalReferenceAttachmentTypeRegistry } from '../attachment_fram import type { PersistableStateAttachmentTypeRegistry } from '../attachment_framework/persistable_state_registry'; import type { UnifiedAttachmentTypeRegistry } from '../attachment_framework/unified_attachment_registry'; import { commentAttachmentType } from '../attachment_framework/attachments'; +import { jsonValueRt } from '../../common/api/runtime_types'; export const registerInternalAttachments = ( externalRefRegistry: ExternalReferenceAttachmentTypeRegistry, @@ -21,7 +23,7 @@ export const registerInternalAttachments = ( unifiedRegistry: UnifiedAttachmentTypeRegistry ) => { externalRefRegistry.register({ id: FILE_ATTACHMENT_TYPE, schemaValidator }); - persistableStateRegistry.register({ id: LENS_ATTACHMENT_TYPE }); + unifiedRegistry.register({ id: LENS_ATTACHMENT_TYPE, schemaValidator: lensSchemaValidator }); unifiedRegistry.register(commentAttachmentType); }; @@ -32,3 +34,11 @@ const schemaValidator = (data: unknown): void => { throw badRequest('Only a single file can be stored in an attachment'); } }; + +const LensAttachmentDataRt = rt.strict({ + state: rt.record(rt.string, jsonValueRt), +}); + +const lensSchemaValidator = (data: unknown): void => { + decodeWithExcessOrThrow(LensAttachmentDataRt)(data); +}; diff --git a/x-pack/platform/test/cases_api_integration/common/plugins/cases/server/routes.ts b/x-pack/platform/test/cases_api_integration/common/plugins/cases/server/routes.ts index 8423c275dcf4f..584ad9da109e2 100644 --- a/x-pack/platform/test/cases_api_integration/common/plugins/cases/server/routes.ts +++ b/x-pack/platform/test/cases_api_integration/common/plugins/cases/server/routes.ts @@ -36,6 +36,10 @@ const getPersistableStateAttachmentTypeHash = (type: PersistableStateAttachmentT return hashParts([type.id]); }; +const getUnifiedAttachmentTypeHash = (type: { id: string }) => { + return hashParts([type.id]); +}; + export const registerRoutes = (core: CoreSetup, logger: Logger) => { const router = core.http.createRouter(); /** @@ -138,6 +142,39 @@ export const registerRoutes = (core: CoreSetup, logger: Logger } ); + router.get( + { + path: '/api/cases_fixture/registered_unified_attachments', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, + validate: {}, + }, + async (context, request, response) => { + try { + const [_, { cases }] = await core.getStartServices(); + const unifiedAttachmentTypeRegistry = cases.getUnifiedAttachmentTypeRegistry(); + + const allTypes = unifiedAttachmentTypeRegistry.list(); + + const hashMap = allTypes.reduce((map, type) => { + map[type.id] = getUnifiedAttachmentTypeHash(type); + return map; + }, {} as Record); + + return response.ok({ + body: hashMap, + }); + } catch (error) { + logger.error(`Error : ${error}`); + throw error; + } + } + ); + /** * This is a fake route to handle deprecated getAllComments method which returns all comments * where as findComments returns only comments of type 'user' 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 new file mode 100644 index 0000000000000..0e32a9b38593f --- /dev/null +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/attachments_framework/registered_unified_attachment_types.ts @@ -0,0 +1,39 @@ +/* + * 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', + }); + }); + }); + }); +}; 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_persistable_state_basic.ts index 8c4e2e991117f..023ff4151835f 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_persistable_state_basic.ts @@ -32,7 +32,6 @@ export default ({ getService }: FtrProviderContext): void => { const types = await getRegisteredTypes(); expect(types).to.eql({ - '.lens': '78559fd806809ac3a1008942ead2a079864054f5', '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', }); }); 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 445fe4f9b726c..1d8494efebd41 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 @@ -32,6 +32,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { 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')); /** * 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 index 9cb7ecd778eab..325526fc9a1e9 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 @@ -32,7 +32,6 @@ export default ({ getService }: FtrProviderContext): void => { const types = await getRegisteredTypes(); expect(types).to.eql({ - '.lens': '78559fd806809ac3a1008942ead2a079864054f5', '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147', 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 cab413e98c670..d8d84ed00bc14 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 @@ -33,6 +33,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { 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')); // sub privileges are only available with a license above basic loadTestFile(require.resolve('./delete_sub_privilege')); loadTestFile(require.resolve('./create_comment_sub_privilege.ts')); 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 df890ddbe4834..02cd52311085a 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 @@ -428,7 +428,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); expect(await title.getVisibleText()).toEqual(caseTitle); - await testSubjects.existOrFail('comment-persistableState-.lens'); + await testSubjects.existOrFail('comment-lens-lens'); }); it('adds lens visualization to an existing case from dashboard', async () => { @@ -454,7 +454,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); expect(await title.getVisibleText()).toEqual(theCaseTitle); - await testSubjects.existOrFail('comment-persistableState-.lens'); + await testSubjects.existOrFail('comment-lens-lens'); }); }); }); diff --git a/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/hooks/use_add_to_case.test.tsx b/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/hooks/use_add_to_case.test.tsx index 3821c703e7cee..bce2fafff5353 100644 --- a/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/hooks/use_add_to_case.test.tsx +++ b/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/hooks/use_add_to_case.test.tsx @@ -11,6 +11,7 @@ import { render } from '../rtl_helpers'; import { EuiButton } from '@elastic/eui'; import { fireEvent } from '@testing-library/react'; import { act } from '@testing-library/react'; +import { LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; describe('useAddToCase', function () { function setupTestComponent() { @@ -75,7 +76,16 @@ describe('useAddToCase', function () { expect(core.http?.post).toHaveBeenCalledTimes(1); expect(core.http?.post).toHaveBeenCalledWith('/api/cases/test/comments', { - body: '{"persistableStateAttachmentState":{"attributes":{"title":"Test lens attributes"},"timeRange":{"to":"now","from":"now-5m"}},"persistableStateAttachmentTypeId":".lens","type":"persistableState","owner":"observability"}', + body: JSON.stringify({ + type: LENS_ATTACHMENT_TYPE, + data: { + state: { + attributes: { title: 'Test lens attributes' }, + timeRange: { to: 'now', from: 'now-5m' }, + }, + }, + owner: 'observability', + }), }); }); }); diff --git a/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/hooks/use_add_to_case.ts b/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/hooks/use_add_to_case.ts index 796bc7949b811..b33100e9e801a 100644 --- a/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/hooks/use_add_to_case.ts +++ b/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/hooks/use_add_to_case.ts @@ -9,7 +9,6 @@ import { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import type { HttpSetup, MountPoint } from '@kbn/core/public'; import type { CaseUI } from '@kbn/cases-plugin/common'; -import { AttachmentType } from '@kbn/cases-plugin/common'; import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { CasesDeepLinkId, DRAFT_COMMENT_STORAGE_ID } from '@kbn/cases-plugin/public'; import { observabilityFeatureId } from '@kbn/observability-shared-plugin/public'; @@ -28,9 +27,8 @@ async function addToCase( const apiPath = `/api/cases/${theCase?.id}/comments`; const payload = { - persistableStateAttachmentState: { attributes, timeRange }, - persistableStateAttachmentTypeId: LENS_ATTACHMENT_TYPE, - type: AttachmentType.persistableState, + type: LENS_ATTACHMENT_TYPE, + data: { state: { attributes, timeRange } }, owner: owner ?? observabilityFeatureId, }; diff --git a/x-pack/solutions/observability/test/serverless/functional/test_suites/cases/attachment_framework.ts b/x-pack/solutions/observability/test/serverless/functional/test_suites/cases/attachment_framework.ts index 4f097a312c89d..fdb62aa06523c 100644 --- a/x-pack/solutions/observability/test/serverless/functional/test_suites/cases/attachment_framework.ts +++ b/x-pack/solutions/observability/test/serverless/functional/test_suites/cases/attachment_framework.ts @@ -96,8 +96,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); expect(await title.getVisibleText()).toEqual(caseTitle); - await retry.waitFor('wait for the visualization to exist', async () => { - return testSubjects.exists('comment-persistableState-.lens'); + await retry.waitFor('lens attachment comment renders after navigation', async () => { + return testSubjects.exists('comment-lens-lens'); }); }); @@ -129,7 +129,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); expect(await title.getVisibleText()).toEqual(theCaseTitle); - await testSubjects.existOrFail('comment-persistableState-.lens'); + await testSubjects.existOrFail('comment-lens-lens'); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx index 95b09b05414bc..105ccbbabf346 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx @@ -14,7 +14,7 @@ import { readCasesPermissions, writeCasesPermissions, } from '../../../cases_test_utils'; -import { AttachmentType } from '@kbn/cases-plugin/common'; +import { LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; jest.mock('../../lib/kibana'); @@ -131,13 +131,14 @@ describe('useAddToExistingCase', () => { expect(attachments).toEqual([ { - persistableStateAttachmentState: { - attributes: kpiHostMetricLensAttributes, - timeRange, - metadata: lensMetadata, + type: LENS_ATTACHMENT_TYPE, + data: { + state: { + attributes: kpiHostMetricLensAttributes, + timeRange, + metadata: lensMetadata, + }, }, - persistableStateAttachmentTypeId: '.lens', - type: AttachmentType.persistableState as const, }, ]); expect(mockClick).toHaveBeenCalled(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx index c07bbd651316a..51c04669c3ab5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx @@ -5,7 +5,7 @@ * 2.0. */ import { useCallback, useMemo } from 'react'; -import { AttachmentType, LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; +import { LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; import type { LensProps } from '@kbn/cases-plugin/public/types'; @@ -29,13 +29,14 @@ export const useAddToExistingCase = ({ const attachments = useMemo(() => { return [ { - persistableStateAttachmentState: { - attributes: lensAttributes, - timeRange, - metadata: lensMetadata, + type: LENS_ATTACHMENT_TYPE, + data: { + state: { + attributes: lensAttributes, + timeRange, + metadata: lensMetadata, + }, }, - persistableStateAttachmentTypeId: LENS_ATTACHMENT_TYPE, - type: AttachmentType.persistableState as const, }, ] as CaseAttachmentsWithoutOwner; }, [lensAttributes, lensMetadata, timeRange]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx index e282fcb6aef1f..77ff939a36dbf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx @@ -14,7 +14,7 @@ import { readCasesPermissions, writeCasesPermissions, } from '../../../cases_test_utils'; -import { AttachmentType } from '@kbn/cases-plugin/common'; +import { LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; jest.mock('../../lib/kibana/kibana_react'); @@ -116,9 +116,10 @@ describe('useAddToNewCase', () => { expect(mockOpenCaseFlyout).toHaveBeenCalledWith({ attachments: [ { - persistableStateAttachmentState: { attributes: kpiHostMetricLensAttributes, timeRange }, - persistableStateAttachmentTypeId: '.lens', - type: AttachmentType.persistableState as const, + type: LENS_ATTACHMENT_TYPE, + data: { + state: { attributes: kpiHostMetricLensAttributes, timeRange, metadata: undefined }, + }, }, ], }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx index 7803e27b2453f..8094d17a33bc7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx @@ -5,7 +5,7 @@ * 2.0. */ import { useCallback, useMemo } from 'react'; -import { AttachmentType, LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; +import { LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; import type { LensProps } from '@kbn/cases-plugin/public/types'; @@ -32,13 +32,14 @@ export const useAddToNewCase = ({ const attachments = useMemo(() => { return [ { - persistableStateAttachmentState: { - attributes: lensAttributes, - timeRange, - metadata: lensMetadata, + type: LENS_ATTACHMENT_TYPE, + data: { + state: { + attributes: lensAttributes, + timeRange, + metadata: lensMetadata, + }, }, - persistableStateAttachmentTypeId: LENS_ATTACHMENT_TYPE, - type: AttachmentType.persistableState as const, }, ] as CaseAttachmentsWithoutOwner; }, [lensAttributes, lensMetadata, timeRange]); diff --git a/x-pack/solutions/security/test/serverless/functional/test_suites/ftr/cases/attachment_framework.ts b/x-pack/solutions/security/test/serverless/functional/test_suites/ftr/cases/attachment_framework.ts index 729da05e2ff4c..0e86e6e33584b 100644 --- a/x-pack/solutions/security/test/serverless/functional/test_suites/ftr/cases/attachment_framework.ts +++ b/x-pack/solutions/security/test/serverless/functional/test_suites/ftr/cases/attachment_framework.ts @@ -88,7 +88,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { expect(await title.getVisibleText()).toEqual(caseTitle); await retry.waitFor('wait for the visualization to exist', async () => { - return testSubjects.exists('comment-persistableState-.lens'); + return testSubjects.exists('comment-lens-lens'); }); }); @@ -127,7 +127,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); expect(await title.getVisibleText()).toEqual(theCaseTitle); - await testSubjects.existOrFail('comment-persistableState-.lens'); + await testSubjects.existOrFail('comment-lens-lens'); }); }); });