diff --git a/src/platform/packages/shared/kbn-lens-common/embeddable/types.ts b/src/platform/packages/shared/kbn-lens-common/embeddable/types.ts index 6867434eb77ee..ec535a8c7fd76 100644 --- a/src/platform/packages/shared/kbn-lens-common/embeddable/types.ts +++ b/src/platform/packages/shared/kbn-lens-common/embeddable/types.ts @@ -7,6 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { BehaviorSubject } from 'rxjs'; + +import type { HasSerializedChildState } from '@kbn/presentation-containers'; import type { AggregateQuery, ExecutionContextSearch, @@ -44,6 +47,7 @@ import type { PublishingSubject, SerializedTitles, ViewMode, + useSearchApi, } from '@kbn/presentation-publishing'; import type { Action } from '@kbn/ui-actions-plugin/public'; import type { @@ -126,9 +130,9 @@ export interface PreventableEvent { preventDefault(): void; } -interface LensByValue { - // by-value - attributes?: Simplify; +export interface LensByValueBase { + savedObjectId?: string; // really should be never but creates type issues + attributes?: LensSavedObjectAttributes; } export interface LensOverrides { @@ -150,10 +154,9 @@ export interface LensOverrides { /** * Lens embeddable props broken down by type */ - -export interface LensByReference { - // by-reference +interface LensByReferenceBase { savedObjectId?: string; + attributes?: never; } interface ContentManagementProps { @@ -161,9 +164,12 @@ interface ContentManagementProps { managed?: boolean; } -export type LensPropsVariants = (LensByValue & LensByReference) & { +interface LensWithReferences { + /** + * @deprecated use `state.attributes.references` + */ references?: Reference[]; -}; +} export interface ViewInDiscoverCallbacks extends LensApiProps { canViewUnderlyingData$: PublishingSubject; @@ -259,9 +265,9 @@ interface LensRequestHandlersProps { * * Panel settings * * other props from the embeddable */ -export type LensSerializedState = Simplify< - LensPropsVariants & - LensOverrides & +type LensSerializedSharedState = Simplify< + LensOverrides & + LensWithReferences & LensUnifiedSearchContext & LensPanelProps & SerializedTitles & @@ -269,6 +275,19 @@ export type LensSerializedState = Simplify< Partial & { isNewPanel?: boolean } >; +export type LensByValueSerializedState = Simplify; +export type LensByRefSerializedState = Simplify; + +/** + * Combined properties of serialized state stored on dashboard panel + * + * Includes: + * - Lens document state (for by-value) + * - Panel settings + * - other props from the embeddable + */ +export type LensSerializedState = LensByRefSerializedState | LensByValueSerializedState; + /** * Custom props exposed on the Lens exported component */ @@ -516,3 +535,26 @@ export interface ESQLVariablesCompatibleDashboardApi { controlGroupApi$: PublishingSubject | undefined>; children$: PublishingSubject<{ [key: string]: unknown }>; } + +type SearchApi = ReturnType; + +interface GeneralLensApi { + searchSessionId$: BehaviorSubject; + disabledActionIds$: BehaviorSubject; + setDisabledActionIds: (ids: string[] | undefined) => void; + viewMode$: BehaviorSubject; + settings: { + syncColors$: BehaviorSubject; + syncCursor$: BehaviorSubject; + syncTooltips$: BehaviorSubject; + }; + forceDSL?: boolean; + esqlVariables$: BehaviorSubject; + hideTitle$: BehaviorSubject; + reload$: BehaviorSubject; +} + +export type LensParentApi = SearchApi & + LensRuntimeState & + GeneralLensApi & + HasSerializedChildState; diff --git a/src/platform/packages/shared/kbn-lens-common/index.ts b/src/platform/packages/shared/kbn-lens-common/index.ts index 88a2cbe71d483..d26c9e57ea5e7 100644 --- a/src/platform/packages/shared/kbn-lens-common/index.ts +++ b/src/platform/packages/shared/kbn-lens-common/index.ts @@ -252,8 +252,8 @@ export type { DocumentToExpressionReturnType, PreventableEvent, LensOverrides, - LensByReference, - LensPropsVariants, + LensByValueSerializedState, + LensByRefSerializedState, ViewInDiscoverCallbacks, IntegrationCallbacks, LensPublicCallbacks, @@ -268,6 +268,7 @@ export type { LensHasEditPanel, LensInspectorAdapters, LensApi, + LensParentApi, LensInternalApi, ExpressionWrapperProps, GetStateType, @@ -276,6 +277,7 @@ export type { TypedLensSerializedState, LensEmbeddableOutput, ESQLVariablesCompatibleDashboardApi, + LensByValueBase, } from './embeddable/types'; export type { LensAppLocatorParams, diff --git a/src/platform/packages/shared/kbn-lens-common/types.ts b/src/platform/packages/shared/kbn-lens-common/types.ts index cb10e18d15228..674410b8dacf5 100644 --- a/src/platform/packages/shared/kbn-lens-common/types.ts +++ b/src/platform/packages/shared/kbn-lens-common/types.ts @@ -79,11 +79,7 @@ import type { InspectorOptions } from '@kbn/inspector-plugin/public'; import type { OnSaveProps } from '@kbn/saved-objects-plugin/public'; import type { NavigateToLensContext } from './convert_to_lens_types'; import type { LensAppLocator, MainHistoryLocationState } from './locator_types'; -import type { - LensRuntimeState, - LensSavedObjectAttributes, - StructuredDatasourceStates, -} from './embeddable/types'; +import type { LensSavedObjectAttributes, StructuredDatasourceStates } from './embeddable/types'; import type { DimensionLink, LensConfiguration, @@ -144,14 +140,6 @@ export interface LensAttributesService { savedObjectId?: string ) => Promise; checkForDuplicateTitle: (props: CheckDuplicateTitleProps) => Promise<{ isDuplicate: boolean }>; - injectReferences: ( - runtimeState: LensRuntimeState, - references: Reference[] | undefined - ) => LensRuntimeState; - extractReferences: (runtimeState: LensRuntimeState) => { - rawState: LensRuntimeState; - references: Reference[]; - }; } export interface LensAppServices extends StartServices { diff --git a/src/platform/packages/shared/kbn-lens-embeddable-utils/package.json b/src/platform/packages/shared/kbn-lens-embeddable-utils/package.json index 71b5c4008a1d9..a225a8c3dedc4 100644 --- a/src/platform/packages/shared/kbn-lens-embeddable-utils/package.json +++ b/src/platform/packages/shared/kbn-lens-embeddable-utils/package.json @@ -3,5 +3,6 @@ "private": true, "version": "1.0.0", "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0", - "homepage": "https://docs.elastic.dev/kibana-dev-docs/lens/config-builder" + "homepage": "https://docs.elastic.dev/kibana-dev-docs/lens/config-builder", + "sideEffects": false } \ No newline at end of file diff --git a/x-pack/platform/plugins/shared/cases/server/common/utils.ts b/x-pack/platform/plugins/shared/cases/server/common/utils.ts index 9ae93b099c543..f4949e3c1b50a 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/utils.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/utils.ts @@ -15,6 +15,7 @@ import type { import { flatMap, uniqWith, xorWith } from 'lodash'; import type { LensServerPluginSetup } from '@kbn/lens-plugin/server'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; +import type { LensEmbeddableStateWithType } from '@kbn/lens-plugin/server/embeddable/types'; import type { ActionsAttachmentPayload, AlertAttachmentPayload, @@ -402,15 +403,16 @@ export const extractLensReferencesFromCommentString = ( lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'], comment: string ): SavedObjectReference[] => { - const extract = lensEmbeddableFactory()?.extract; + const extract = lensEmbeddableFactory().extract; if (extract) { const parsedComment = parseCommentString(comment); const lensVisualizations = getLensVisualizations(parsedComment.children); - const flattenRefs = flatMap( - lensVisualizations, - (lensObject) => extract(lensObject)?.references ?? [] - ); + const flattenRefs = flatMap(lensVisualizations, (vis) => { + // TODO: Improve these types + const lensVis = vis as unknown as LensEmbeddableStateWithType; + return extract(lensVis).references; + }); const uniqRefs = uniqWith( flattenRefs, diff --git a/x-pack/platform/plugins/shared/lens/common/embeddable_factory/index.ts b/x-pack/platform/plugins/shared/lens/common/embeddable_factory/index.ts deleted file mode 100644 index 18cc02c08fe69..0000000000000 --- a/x-pack/platform/plugins/shared/lens/common/embeddable_factory/index.ts +++ /dev/null @@ -1,58 +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 { cloneDeep } from 'lodash'; -import type { SerializableRecord } from '@kbn/utility-types'; -import type { Reference } from '@kbn/content-management-utils'; -import type { - EmbeddableRegistryDefinition, - EmbeddableStateWithType, -} from '@kbn/embeddable-plugin/common'; -import type { LensRuntimeState } from '../../public'; - -export type LensEmbeddablePersistableState = EmbeddableStateWithType & { - attributes: SerializableRecord; -}; - -export const inject: NonNullable = ( - state, - references -): EmbeddableStateWithType => { - const typedState = cloneDeep(state) as unknown as LensRuntimeState; - - if (typedState.savedObjectId) { - return typedState as unknown as EmbeddableStateWithType; - } - - // match references based on name, so only references associated with this lens panel are injected. - const matchedReferences: Reference[] = []; - - if (Array.isArray(typedState.attributes.references)) { - typedState.attributes.references.forEach((serializableRef) => { - const internalReference = serializableRef; - const matchedReference = references.find( - (reference) => reference.name === internalReference.name - ); - if (matchedReference) matchedReferences.push(matchedReference); - }); - } - - typedState.attributes.references = matchedReferences; - - return typedState as unknown as EmbeddableStateWithType; -}; - -export const extract: NonNullable = (state) => { - let references: Reference[] = []; - const typedState = state as unknown as LensRuntimeState; - - if ('attributes' in typedState && typedState.attributes !== undefined) { - references = typedState.attributes.references; - } - - return { state, references }; -}; diff --git a/x-pack/platform/plugins/shared/lens/common/references/index.ts b/x-pack/platform/plugins/shared/lens/common/references/index.ts new file mode 100644 index 0000000000000..9d1745e765385 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/common/references/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cloneDeep, uniqBy } from 'lodash'; + +import type { Reference } from '@kbn/content-management-utils'; + +import type { LensSerializedState } from '../../public'; + +export const injectLensReferences = ( + state: LensSerializedState, + references: Reference[] = [] +): LensSerializedState => { + const clonedState = cloneDeep(state); + + if (clonedState.savedObjectId || !clonedState.attributes) { + return clonedState; + } + + // TODO: find a way to cull erroneous dashboard references + const combinedReferences = uniqBy([...references, ...clonedState.attributes.references], 'name'); + + clonedState.attributes.references = combinedReferences; + + return clonedState; +}; + +export const extractLensReferences = ( + state: LensSerializedState +): { + state: LensSerializedState; + references: Reference[]; +} => { + return { + state, + references: state.attributes?.references ?? state.references ?? [], + }; +}; diff --git a/x-pack/platform/plugins/shared/lens/common/transforms/config_builder_stub.ts b/x-pack/platform/plugins/shared/lens/common/transforms/config_builder_stub.ts deleted file mode 100644 index e0cfd5f89ea5b..0000000000000 --- a/x-pack/platform/plugins/shared/lens/common/transforms/config_builder_stub.ts +++ /dev/null @@ -1,38 +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 { LensAPIConfig, LensItem } from '../../server/content_management'; - -export function isNewApiFormat(config: unknown): config is LensAPIConfig { - return (config as LensAPIConfig)?.state?.isNewApiFormat; -} - -export const ConfigBuilderStub = { - /** - * @returns Lens item - */ - in(config: LensAPIConfig): LensItem { - const { isNewApiFormat: _, ...cleanedState } = config.state; - return { - ...config, - state: cleanedState, - }; - }, - - /** - * @returns Lens API config - */ - out(item: LensItem): LensAPIConfig { - return { - ...item, - state: { - ...item.state, - isNewApiFormat: true, - }, - }; - }, -}; diff --git a/x-pack/platform/plugins/shared/lens/common/transforms/index.ts b/x-pack/platform/plugins/shared/lens/common/transforms/index.ts index f294802fed3ed..c8e3d24929c15 100644 --- a/x-pack/platform/plugins/shared/lens/common/transforms/index.ts +++ b/x-pack/platform/plugins/shared/lens/common/transforms/index.ts @@ -5,4 +5,21 @@ * 2.0. */ -export { ConfigBuilderStub } from './config_builder_stub'; +import type { EnhancementsRegistry } from '@kbn/embeddable-plugin/common/enhancements/registry'; + +import type { LensTransforms } from './types'; +import { getTransformIn } from './transform_in'; +import { getTransformOut } from './transform_out'; + +export interface LensTransformDependencies { + transformEnhancementsIn?: EnhancementsRegistry['transformIn']; + transformEnhancementsOut?: EnhancementsRegistry['transformOut']; +} + +export function getLensTransforms(deps: LensTransformDependencies): LensTransforms { + return { + transformIn: getTransformIn(deps), + transformOut: getTransformOut(deps), + transformOutInjectsReferences: true, + }; +} diff --git a/x-pack/platform/plugins/shared/lens/common/transforms/transform_in.ts b/x-pack/platform/plugins/shared/lens/common/transforms/transform_in.ts new file mode 100644 index 0000000000000..921cf24661d09 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/common/transforms/transform_in.ts @@ -0,0 +1,53 @@ +/* + * 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 { LensTransformDependencies } from '.'; +import { DOC_TYPE } from '../constants'; +import { extractLensReferences } from '../references'; +import type { + LensByRefTransformInResult, + LensByValueTransformInResult, + LensTransformIn, +} from './types'; +import { LENS_SAVED_OBJECT_REF_NAME, isByRefLensState } from './utils'; + +/** + * Transform from Lens API format to Lens Serialized State + */ +export const getTransformIn = ({ + transformEnhancementsIn, +}: LensTransformDependencies): LensTransformIn => { + return function transformIn(state) { + const { enhancementsState: enhancements = null, enhancementsReferences = [] } = + state.enhancements ? transformEnhancementsIn?.(state.enhancements) ?? {} : {}; + const enhancementsState = enhancements ? { enhancements } : {}; + + if (isByRefLensState(state)) { + const { savedObjectId: id, ...rest } = state; + return { + state: rest, + ...enhancementsState, + references: [ + { + name: LENS_SAVED_OBJECT_REF_NAME, + type: DOC_TYPE, + id: id!, + }, + ...enhancementsReferences, + ], + } satisfies LensByRefTransformInResult; + } + + const { state: lensState, references: lensReferences } = extractLensReferences(state); + + return { + state: lensState, + ...enhancementsState, + references: [...lensReferences, ...enhancementsReferences], + } satisfies LensByValueTransformInResult; + }; +}; diff --git a/x-pack/platform/plugins/shared/lens/common/transforms/transform_out.ts b/x-pack/platform/plugins/shared/lens/common/transforms/transform_out.ts new file mode 100644 index 0000000000000..e1ed8a33a922a --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/common/transforms/transform_out.ts @@ -0,0 +1,83 @@ +/* + * 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 { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public'; +import type { LensByValueSerializedState } from '@kbn/lens-common'; +import type { LensTransformDependencies } from '.'; +import { LENS_ITEM_VERSION_V1, transformToV1LensItemAttributes } from '../content_management/v1'; +import { injectLensReferences } from '../references'; +import type { + LensByRefTransformOutResult, + LensByValueTransformOutResult, + LensTransformOut, +} from './types'; +import { findLensReference, isByRefLensState } from './utils'; + +/** + * Transform from Lens Serialized State to Lens API format + */ +export const getTransformOut = ({ + transformEnhancementsOut, +}: LensTransformDependencies): LensTransformOut => { + return function transformOut(state, references) { + const enhancements = state.enhancements + ? transformEnhancementsOut?.(state.enhancements, references ?? []) + : undefined; + const enhancementsState = ( + enhancements ? { enhancements } : {} + ) as DynamicActionsSerializedState; + + const savedObjectRef = findLensReference(references); + + if (savedObjectRef && isByRefLensState(state)) { + return { + ...state, + ...enhancementsState, + savedObjectId: savedObjectRef.id, + } satisfies LensByRefTransformOutResult; + } + + const migratedAttributes = migrateAttributes(state.attributes); + const injectedState = injectLensReferences( + { + ...state, + ...enhancementsState, + attributes: migratedAttributes, + }, + references + ); + + return injectedState satisfies LensByValueTransformOutResult; + }; +}; + +/** + * Handles transforming old lens SO in dashboard to v1 Lens SO + */ +function migrateAttributes(attributes: LensByValueSerializedState['attributes']) { + if (!attributes) { + throw new Error('Why are attributes undefined?'); + } + + const { visualizationType } = attributes; + + if (!visualizationType) { + throw new Error('Missing visualizationType'); + } + + const version = attributes.version ?? 0; + + let newAttributes = { ...attributes }; + if (version < LENS_ITEM_VERSION_V1) { + newAttributes = { + ...newAttributes, + ...transformToV1LensItemAttributes({ ...attributes, visualizationType }), + }; + } + + return newAttributes; +} diff --git a/x-pack/platform/plugins/shared/lens/common/transforms/types.ts b/x-pack/platform/plugins/shared/lens/common/transforms/types.ts new file mode 100644 index 0000000000000..2a69e8b26e39f --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/common/transforms/types.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EmbeddableTransforms } from '@kbn/embeddable-plugin/common'; +import type { Required } from 'utility-types'; + +import type { + LensSerializedState, + LensByRefSerializedState, + LensByValueSerializedState, +} from '@kbn/lens-common'; + +export type LensTransforms = Required< + EmbeddableTransforms, + 'transformIn' | 'transformOut' +>; + +/** + * Transform from Lens API format to Lens Serialized State + */ +export type LensTransformIn = LensTransforms['transformIn']; + +/** + * Transform from to Lens Serialized State to Lens API format + */ +export type LensTransformOut = LensTransforms['transformOut']; + +type LensByRefTransforms = Required< + EmbeddableTransforms +>; +export type LensByRefTransformInResult = ReturnType; +export type LensByRefTransformOutResult = ReturnType; + +type LensByValueTransforms = Required< + EmbeddableTransforms +>; +export type LensByValueTransformInResult = ReturnType; +export type LensByValueTransformOutResult = ReturnType; diff --git a/x-pack/platform/plugins/shared/lens/common/transforms/utils.ts b/x-pack/platform/plugins/shared/lens/common/transforms/utils.ts new file mode 100644 index 0000000000000..19dc9e432d920 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/common/transforms/utils.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Reference } from '@kbn/content-management-utils'; + +import type { LensByRefSerializedState, LensSerializedState } from '@kbn/lens-common'; +import { DOC_TYPE } from '../constants'; + +export const LENS_SAVED_OBJECT_REF_NAME = 'savedObjectRef'; + +export function findLensReference(references?: Reference[]) { + return references + ? references.find((ref) => ref.type === DOC_TYPE && ref.name === LENS_SAVED_OBJECT_REF_NAME) + : undefined; +} + +export function isByRefLensState(state: LensSerializedState): state is LensByRefSerializedState { + return !state.attributes; +} diff --git a/x-pack/platform/plugins/shared/lens/public/app_plugin/mounter.tsx b/x-pack/platform/plugins/shared/lens/public/app_plugin/mounter.tsx index a7cda815a5764..545a7a7de0f4f 100644 --- a/x-pack/platform/plugins/shared/lens/public/app_plugin/mounter.tsx +++ b/x-pack/platform/plugins/shared/lens/public/app_plugin/mounter.tsx @@ -19,7 +19,6 @@ import { Storage, withNotifyOnErrors, } from '@kbn/kibana-utils-plugin/public'; -import type { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import { ACTION_VISUALIZE_LENS_FIELD } from '@kbn/ui-actions-plugin/public'; import { ACTION_CONVERT_TO_LENS } from '@kbn/visualizations-plugin/public'; @@ -42,7 +41,8 @@ import type { import { LENS_SHARE_STATE_ACTION } from '@kbn/lens-common'; import { App } from './app'; import { addHelpMenuToAppChrome } from '../help_menu_util'; -import { extract } from '../../common/embeddable_factory'; +import type { LensPluginStartDependencies } from '../plugin'; +import { extractLensReferences } from '../../common/references'; import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common/constants'; import type { RedirectToOriginProps, HistoryLocationState } from './types'; import type { LensRootStore } from '../state_management'; @@ -50,7 +50,6 @@ import { makeConfigureStore, navigateAway, loadInitial, setState } from '../stat import { getPreloadedState } from '../state_management/lens_slice'; import { getLensInspectorService } from '../lens_inspector_service'; import { LensDocumentService } from '../persistence'; -import type { LensPluginStartDependencies } from '../plugin'; import { EditorFrameServiceProvider } from '../editor_frame_service/editor_frame_service_context'; function getInitialContext(history: AppMountParameters['history']) { @@ -219,7 +218,7 @@ export async function mountApp( } if (stateTransfer && props?.state) { const { state: rawState, isCopied } = props; - const { references } = extract(rawState as unknown as EmbeddableStateWithType); + const { references } = extractLensReferences(rawState); stateTransfer.navigateToWithEmbeddablePackages(mergedOriginatingApp, { path: embeddableEditorIncomingState?.originatingPath, state: [ diff --git a/x-pack/platform/plugins/shared/lens/public/app_plugin/save_modal_container_helpers.ts b/x-pack/platform/plugins/shared/lens/public/app_plugin/save_modal_container_helpers.ts index f72b2d19b30b1..259d857ae16b8 100644 --- a/x-pack/platform/plugins/shared/lens/public/app_plugin/save_modal_container_helpers.ts +++ b/x-pack/platform/plugins/shared/lens/public/app_plugin/save_modal_container_helpers.ts @@ -15,10 +15,9 @@ import { DEFAULT_AUTO_APPLY_SELECTIONS, CONTROLS_GROUP_TYPE, } from '@kbn/controls-constants'; -import type { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; import type { LensAppServices, LensSerializedState } from '@kbn/lens-common'; import { LENS_EMBEDDABLE_TYPE } from '../../common/constants'; -import { extract } from '../../common/embeddable_factory'; +import { extractLensReferences } from '../../common/references'; /** * Transforms control panels state into controls group state format. @@ -61,7 +60,7 @@ export const redirectToDashboard = ({ stateTransfer: LensAppServices['stateTransfer']; controlsState?: ControlPanelsState; }) => { - const { references } = extract(rawState as unknown as EmbeddableStateWithType); + const { references } = extractLensReferences(rawState); const appId = originatingApp || 'dashboards'; diff --git a/x-pack/platform/plugins/shared/lens/public/lens_attribute_service.ts b/x-pack/platform/plugins/shared/lens/public/lens_attribute_service.ts index 605ce58c24800..473f2b56d0011 100644 --- a/x-pack/platform/plugins/shared/lens/public/lens_attribute_service.ts +++ b/x-pack/platform/plugins/shared/lens/public/lens_attribute_service.ts @@ -9,15 +9,12 @@ import type { Reference } from '@kbn/content-management-utils'; import type { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; import { noop } from 'lodash'; import type { HttpStart } from '@kbn/core/public'; -import type { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; import type { SharingSavedObjectProps, - LensRuntimeState, LensSavedObjectAttributes, CheckDuplicateTitleProps, LensAttributesService, } from '@kbn/lens-common'; -import { extract, inject } from '../common/embeddable_factory'; import { LensDocumentService } from './persistence'; import { DOC_TYPE } from '../common/constants'; @@ -94,19 +91,5 @@ export function getLensAttributeService(http: HttpStart): LensAttributesService ), }; }, - // Make sure to inject references from the container down to the runtime state - // this ensure migrations/copy to spaces works correctly - injectReferences: (runtimeState, references) => { - return inject( - runtimeState as unknown as EmbeddableStateWithType, - references ?? runtimeState.attributes.references - ) as unknown as LensRuntimeState; - }, - // Make sure to move the internal references into the parent references - // so migrations/move to spaces can work properly - extractReferences: (runtimeState) => { - const { state, references } = extract(runtimeState as unknown as EmbeddableStateWithType); - return { rawState: state as unknown as LensRuntimeState, references }; - }, }; } diff --git a/x-pack/platform/plugins/shared/lens/public/mocks/services_mock.tsx b/x-pack/platform/plugins/shared/lens/public/mocks/services_mock.tsx index 6ea013aa2037a..bd4a682885f88 100644 --- a/x-pack/platform/plugins/shared/lens/public/mocks/services_mock.tsx +++ b/x-pack/platform/plugins/shared/lens/public/mocks/services_mock.tsx @@ -64,17 +64,6 @@ export function makeAttributeService(doc: LensDocument): jest.Mocked ({ - ..._runtimeState, - attributes: { - ..._runtimeState.attributes, - references: references?.length ? references : _runtimeState.attributes.references, - }, - })), - extractReferences: jest.fn((_runtimeState) => ({ - rawState: _runtimeState, - references: _runtimeState.attributes.references || [], - })), }; return attributeServiceMock; diff --git a/x-pack/platform/plugins/shared/lens/public/persistence/lens_client.ts b/x-pack/platform/plugins/shared/lens/public/persistence/lens_client.ts index 43fd3f690e1c4..a19931ac50ff3 100644 --- a/x-pack/platform/plugins/shared/lens/public/persistence/lens_client.ts +++ b/x-pack/platform/plugins/shared/lens/public/persistence/lens_client.ts @@ -8,11 +8,9 @@ import type { HttpStart } from '@kbn/core/public'; import type { Reference } from '@kbn/content-management-utils'; -import { omit } from 'lodash'; import type { LensSavedObjectAttributes } from '@kbn/lens-common'; import { LENS_API_VERSION, LENS_VIS_API_PATH } from '../../common/constants'; import type { LensAttributes, LensItem } from '../../server/content_management'; -import { ConfigBuilderStub } from '../../common/transforms'; import { type LensGetResponseBody, type LensCreateRequestBody, @@ -22,6 +20,16 @@ import { type LensSearchRequestQuery, type LensSearchResponseBody, } from '../../server'; +import type { + LensCreateRequestQuery, + LensItemMeta, + LensUpdateRequestQuery, +} from '../../server/api/routes/visualizations/types'; + +export interface LensItemResponse = {}> { + item: LensItem; + meta: LensItemMeta & M; +} /** * This type is to allow `visualizationType` to be `null` in the public context. @@ -34,50 +42,56 @@ export type LooseLensAttributes = Omit & export class LensClient { constructor(private http: HttpStart) {} - async get(id: string) { - const { data, meta } = await this.http.get(`${LENS_VIS_API_PATH}/${id}`, { + async get(id: string): Promise> { + const { + data, + meta, + id: responseId, + } = await this.http.get(`${LENS_VIS_API_PATH}/${id}`, { version: LENS_API_VERSION, }); return { - item: ConfigBuilderStub.in(data), - meta, // TODO: see if we still need this meta data + item: { + ...data, + id: responseId, + }, + meta, }; } async create( { description, visualizationType, state, title, version }: LooseLensAttributes, references: Reference[], - options: LensCreateRequestBody['options'] = {} - ) { + options: LensCreateRequestQuery = {} + ): Promise { if (visualizationType === null) { throw new Error('Missing visualization type'); } const body: LensCreateRequestBody = { - // TODO: Find a better way to conditionally omit id - data: omit( - ConfigBuilderStub.out({ - id: '', - description, - visualizationType, - state, - title, - version, - references, - }), - 'id' - ), - options, + description, + visualizationType, + state, + title, + version, + references, }; - const { data, meta } = await this.http.post(LENS_VIS_API_PATH, { - body: JSON.stringify(body), - version: LENS_API_VERSION, - }); + const { data, meta, ...rest } = await this.http.post( + LENS_VIS_API_PATH, + { + body: JSON.stringify(body), + query: options, + version: LENS_API_VERSION, + } + ); return { - item: ConfigBuilderStub.in(data), + item: { + ...rest, + ...data, + }, meta, }; } @@ -86,39 +100,35 @@ export class LensClient { id: string, { description, visualizationType, state, title, version }: LooseLensAttributes, references: Reference[], - options: LensUpdateRequestBody['options'] = {} - ) { + options: LensUpdateRequestQuery = {} + ): Promise { if (visualizationType === null) { throw new Error('Missing visualization type'); } const body: LensUpdateRequestBody = { - // TODO: Find a better way to conditionally omit id - data: omit( - ConfigBuilderStub.out({ - id: '', - description, - visualizationType, - state, - title, - version, - references, - }), - 'id' - ), - options, + description, + visualizationType, + state, + title, + version, + references, }; - const { data, meta } = await this.http.put( + const { data, meta, ...rest } = await this.http.put( `${LENS_VIS_API_PATH}/${id}`, { body: JSON.stringify(body), + query: options, version: LENS_API_VERSION, } ); return { - item: ConfigBuilderStub.in(data), + item: { + ...rest, + ...data, + }, meta, }; } @@ -140,7 +150,6 @@ export class LensClient { fields, searchFields, }: LensSearchRequestQuery): Promise { - // TODO add all CM search options to query const result = await this.http.get(LENS_VIS_API_PATH, { query: { query, @@ -152,9 +161,11 @@ export class LensClient { version: LENS_API_VERSION, }); - return result.data.map(({ data }) => ({ - ...data, - attributes: ConfigBuilderStub.in(data), - })); + return result.data.map(({ id, data }) => { + return { + id, + ...data, + } satisfies LensItem; + }); } } diff --git a/x-pack/platform/plugins/shared/lens/public/persistence/utils.ts b/x-pack/platform/plugins/shared/lens/public/persistence/utils.ts index baf82d5253cfc..22037f8fde5e1 100644 --- a/x-pack/platform/plugins/shared/lens/public/persistence/utils.ts +++ b/x-pack/platform/plugins/shared/lens/public/persistence/utils.ts @@ -5,7 +5,8 @@ * 2.0. */ -import type { LensItem, LensItemMeta, LensSavedObject } from '../../server/content_management'; +import type { LensSavedObject } from '../../server/content_management'; +import type { LensItemResponse } from './lens_client'; /** * Converts Lens Response Item to Lens Saved Object @@ -15,10 +16,7 @@ import type { LensItem, LensItemMeta, LensSavedObject } from '../../server/conte export function getLensSOFromResponse({ item: { id, references, ...attributes }, meta: { type, createdAt, updatedAt, createdBy, updatedBy, managed, originId }, -}: { - item: LensItem; - meta: LensItemMeta; -}): LensSavedObject { +}: LensItemResponse): LensSavedObject { return { id, references, diff --git a/x-pack/platform/plugins/shared/lens/public/plugin.ts b/x-pack/platform/plugins/shared/lens/public/plugin.ts index f9bd4cdea318e..e929e0e14ae8c 100644 --- a/x-pack/platform/plugins/shared/lens/public/plugin.ts +++ b/x-pack/platform/plugins/shared/lens/public/plugin.ts @@ -62,6 +62,7 @@ import type { VisualizeEditorContext, EditorFrameSetup, LensDocument, + LensByRefSerializedState, } from '@kbn/lens-common'; import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public'; import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; @@ -385,23 +386,24 @@ export class LensPlugin { return createLensEmbeddableFactory(deps); }); + embeddable.registerLegacyURLTransform(LENS_EMBEDDABLE_TYPE, async () => { + const { getLensTransforms } = await import('../common/transforms'); + return getLensTransforms({ + transformEnhancementsIn: embeddable.transformEnhancementsIn, + transformEnhancementsOut: embeddable.transformEnhancementsOut, + }).transformOut; + }); + // Let Dashboard know about the Lens panel type embeddable.registerAddFromLibraryType({ - onAdd: async (container, savedObject) => { - const { SAVED_OBJECT_REF_NAME } = await import('@kbn/presentation-publishing'); + onAdd: (container, savedObject) => { container.addNewPanel( { panelType: LENS_EMBEDDABLE_TYPE, serializedState: { - rawState: {}, - references: [ - ...savedObject.references, - { - name: SAVED_OBJECT_REF_NAME, - type: LENS_EMBEDDABLE_TYPE, - id: savedObject.id, - }, - ], + rawState: { + savedObjectId: savedObject.id, + } satisfies LensByRefSerializedState, }, }, true diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.test.ts index 38c506a408563..ab32dc7a42c8d 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.test.ts @@ -63,63 +63,6 @@ describe('Embeddable helpers', () => { // check the visualizationType set to null for empty state expect(runtimeState.attributes.visualizationType).toBeNull(); }); - - describe('injected references should overwrite inner ones', () => { - // There are 3 possible scenarios here for reference injections: - // * default space for a by-value - // * default space for a by-ref with a "lens" panel reference type - // * other space for a by-value with new ref ids - - it('should inject correctly serialized references into runtime state for a by value in the default space', async () => { - const services = getServices(); - const mockedReferences = [ - { id: 'serializedRefs', name: 'index-pattern-0', type: 'mocked-reference' }, - ]; - const runtimeState = await deserializeState( - services, - { - attributes: defaultDoc, - }, - mockedReferences - ); - expect(services.attributeService.injectReferences).toHaveBeenCalled(); - expect(runtimeState.attributes.references).toEqual(mockedReferences); - }); - - it('should inject correctly serialized references into runtime state for a by ref in the default space', async () => { - const services = getServices(); - const mockedReferences = [ - { id: 'serializedRefs', name: 'index-pattern-0', type: 'mocked-reference' }, - ]; - const runtimeState = await deserializeState( - services, - { - savedObjectId: '123', - }, - mockedReferences - ); - expect(services.attributeService.injectReferences).not.toHaveBeenCalled(); - // Note the original references should be kept - expect(runtimeState.attributes.references).toEqual(defaultDoc.references); - }); - - it('should inject correctly serialized references into runtime state for a by value in another space', async () => { - const services = getServices(); - const mockedReferences = [ - { id: 'serializedRefs', name: 'index-pattern-0', type: 'mocked-reference' }, - ]; - const runtimeState = await deserializeState( - services, - { - attributes: defaultDoc, - }, - mockedReferences - ); - expect(services.attributeService.injectReferences).toHaveBeenCalled(); - // note: in this case the references are swapped - expect(runtimeState.attributes.references).toEqual(mockedReferences); - }); - }); }); describe('getStructuredDatasourceStates', () => { diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts index abdf36d1479ef..003c77d874680 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { Reference } from '@kbn/content-management-utils'; import type { ViewMode } from '@kbn/presentation-publishing'; import { apiHasParentApi, @@ -13,13 +12,13 @@ import { getInheritedViewMode, type PublishingSubject, apiHasExecutionContext, - findSavedObjectRef, } from '@kbn/presentation-publishing'; import { isObject } from 'lodash'; import { BehaviorSubject } from 'rxjs'; import { isOfAggregateQueryType } from '@kbn/es-query'; import type { RenderMode } from '@kbn/expressions-plugin/common'; import type { + LensByValueSerializedState, LensRuntimeState, LensSerializedState, StructuredDatasourceStates, @@ -30,7 +29,6 @@ import type { } from '@kbn/lens-common'; import type { ESQLStartServices } from './esql'; import { loadESQLAttributes } from './esql'; -import { DOC_TYPE } from '../../common/constants'; import { LENS_ITEM_LATEST_VERSION } from '../../common/constants'; import type { LensEmbeddableStartServices } from './types'; @@ -70,29 +68,28 @@ export async function deserializeState( attributeService, ...services }: Pick & ESQLStartServices, - rawState: LensSerializedState, - references?: Reference[] + { savedObjectId, ...state }: LensSerializedState ): Promise { const fallbackAttributes = createEmptyLensState().attributes; - const savedObjectRef = findSavedObjectRef(DOC_TYPE, references); - const savedObjectId = savedObjectRef?.id ?? rawState.savedObjectId; if (savedObjectId) { try { const { attributes, managed, sharingSavedObjectProps } = await attributeService.loadFromLibrary(savedObjectId); - return { ...rawState, savedObjectId, attributes, managed, sharingSavedObjectProps }; + return { + ...state, + savedObjectId, + attributes, + managed, + sharingSavedObjectProps, + } satisfies LensRuntimeState; } catch (e) { // return an empty Lens document if no saved object is found - return { ...rawState, attributes: fallbackAttributes }; + return { ...state, attributes: fallbackAttributes }; } } - // Inject applied only to by-value SOs - const newState = attributeService.injectReferences( - ('attributes' in rawState ? rawState : { attributes: rawState }) as LensRuntimeState, - references?.length ? references : undefined - ); + const newState = transformInitialState(state) as LensRuntimeState; if (newState.isNewPanel) { try { @@ -164,3 +161,13 @@ export function getStructuredDatasourceStates( undefined) as TextBasedPersistedState, }; } + +export function transformInitialState(state: LensSerializedState): LensSerializedState { + // TODO add api conversion + return state; +} + +export function transformOutputState(state: LensSerializedState): LensByValueSerializedState { + // TODO add api conversion + return state; +} diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts index 39dbd2c0e0445..504b67ff14f1f 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts @@ -28,7 +28,7 @@ import type { LensApi, LensSerializedState, } from '@kbn/lens-common'; -import { isTextBasedLanguage } from '../helper'; +import { isTextBasedLanguage, transformOutputState } from '../helper'; import type { LensEmbeddableStartServices } from '../types'; import { apiHasLensComponentProps } from '../type_guards'; @@ -132,12 +132,18 @@ export function initializeDashboardServices( canUnlinkFromLibrary: async () => Boolean(getLatestState().savedObjectId), getSerializedStateByReference: (newId: string) => { const currentState = getLatestState(); - currentState.savedObjectId = newId; - return attributeService.extractReferences(currentState); + return { + rawState: { + ...currentState, + savedObjectId: newId, + }, + }; }, getSerializedStateByValue: () => { const { savedObjectId, ...byValueRuntimeState } = getLatestState(); - return attributeService.extractReferences(byValueRuntimeState); + return { + rawState: transformOutputState(byValueRuntimeState), + }; }, }, anyStateChange$: merge( diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.test.ts index c9639a24c55cc..2c26c9fe21902 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.test.ts @@ -7,18 +7,13 @@ import { faker } from '@faker-js/faker'; import { createEmptyLensState } from '../helper'; -import { makeEmbeddableServices, getLensRuntimeStateMock } from '../mocks'; +import { getLensRuntimeStateMock } from '../mocks'; import type { LensRuntimeState } from '@kbn/lens-common'; import { initializeIntegrations } from './initialize_integrations'; function setupIntegrationsApi(stateOverrides?: Partial) { - const services = makeEmbeddableServices(undefined, undefined, { - visOverrides: { id: 'lnsXY' }, - dataOverrides: { id: 'formBased' }, - }); const runtimeState = getLensRuntimeStateMock(stateOverrides); - const serializeDynamicActions = undefined; - const { api } = initializeIntegrations(() => runtimeState, serializeDynamicActions, services); + const { api } = initializeIntegrations(() => runtimeState); return api; } @@ -41,7 +36,7 @@ describe('Dashboard services API', () => { // * savedObjectId is cleaned up expect(rawState).not.toHaveProperty('savedObjectId'); // * references should be at root level - expect(references).toEqual(attributes.references); + expect(references).toBeUndefined(); }); it('should serialize state for a by-reference panel', async () => { const attributes = createAttributesWithReferences(); @@ -52,15 +47,7 @@ describe('Dashboard services API', () => { const { rawState, references } = api.serializeState(); // check the same 3 things as above expect(rawState).not.toEqual(expect.objectContaining({ attributes: expect.anything() })); - // * references should be at root level - expect(references).toEqual([ - ...attributes.references, - { - id: '123', - name: 'savedObjectRef', - type: 'lens', - }, - ]); + expect(references).toBeUndefined(); }); it('should remove the searchSessionId from the serializedState', async () => { diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts index cbb30af655b62..e33c0623da583 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts @@ -11,37 +11,24 @@ import { isOfAggregateQueryType, } from '@kbn/es-query'; import { omit } from 'lodash'; -import type { Reference } from '@kbn/content-management-utils'; -import { - SAVED_OBJECT_REF_NAME, - type HasSerializableState, - type SerializedPanelState, -} from '@kbn/presentation-publishing'; -import type { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public'; -import type { GetStateType, IntegrationCallbacks, LensRuntimeState } from '@kbn/lens-common'; -import { isTextBasedLanguage } from '../helper'; -import { DOC_TYPE } from '../../../common/constants'; -import type { LensEmbeddableStartServices } from '../types'; +import { type HasSerializableState, type SerializedPanelState } from '@kbn/presentation-publishing'; +import type { + GetStateType, + LensByRefSerializedState, + LensByValueSerializedState, + LensRuntimeState, + LensSerializedState, + IntegrationCallbacks, +} from '@kbn/lens-common'; +import { isTextBasedLanguage, transformOutputState } from '../helper'; -function cleanupSerializedState({ - rawState, - references, -}: { - rawState: LensRuntimeState; - references: Reference[]; -}) { - const cleanedState = omit(rawState, 'searchSessionId'); - return { - rawState: cleanedState, - references, - }; +function cleanupSerializedState(state: LensRuntimeState) { + const cleanedState = omit(state, 'searchSessionId'); + + return cleanedState; } -export function initializeIntegrations( - getLatestState: GetStateType, - serializeDynamicActions: (() => SerializedPanelState) | undefined, - { attributeService }: LensEmbeddableStartServices -): { +export function initializeIntegrations(getLatestState: GetStateType): { api: Omit< IntegrationCallbacks, | 'updateState' @@ -52,7 +39,7 @@ export function initializeIntegrations( | 'updateDataLoading' | 'getTriggerCompatibleActions' > & - HasSerializableState; + HasSerializableState; } { return { api: { @@ -60,38 +47,25 @@ export function initializeIntegrations( * This API is used by the parent to serialize the panel state to save it into its saved object. * Make sure to remove the attributes when the panel is by reference. */ - serializeState: () => { - const currentState = getLatestState(); - const cleanedState = cleanupSerializedState( - attributeService.extractReferences(currentState) - ); - const { rawState: dynamicActionsState, references: dynamicActionsReferences } = - serializeDynamicActions?.() ?? {}; - if (cleanedState.rawState.savedObjectId) { - const { savedObjectId, attributes, ...byRefState } = cleanedState.rawState; + serializeState: (): SerializedPanelState => { + const currentState = cleanupSerializedState(getLatestState()); + + const { savedObjectId, attributes, ...state } = currentState; + + if (savedObjectId) { return { rawState: { - ...byRefState, - ...dynamicActionsState, + ...state, + savedObjectId, }, - references: [ - ...cleanedState.references, - ...(dynamicActionsReferences ?? []), - { - name: SAVED_OBJECT_REF_NAME, - type: DOC_TYPE, - id: savedObjectId, - }, - ], - }; + } satisfies SerializedPanelState; } + + const transformedState = transformOutputState(currentState); + return { - rawState: { - ...cleanedState.rawState, - ...dynamicActionsState, - }, - references: [...cleanedState.references, ...(dynamicActionsReferences ?? [])], - }; + rawState: transformedState, + } satisfies SerializedPanelState; }, // TODO: workout why we have this duplicated getFullAttributes: () => getLatestState().attributes, diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx index 993589ab7f6df..eefa333968ff7 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx @@ -62,11 +62,7 @@ export const createLensEmbeddableFactory = ( initialState ); - const initialRuntimeState = await deserializeState( - services, - initialState.rawState, - initialState.references - ); + const initialRuntimeState = await deserializeState(services, initialState.rawState); /** * Observables and functions declared here are used internally to store mutating state values @@ -116,11 +112,7 @@ export const createLensEmbeddableFactory = ( parentApi ); - const integrationsConfig = initializeIntegrations( - getLatestState, - dynamicActionsManager?.serializeState, - services - ); + const integrationsConfig = initializeIntegrations(getLatestState); const actionsConfig = initializeActionApi( uuid, initialRuntimeState, @@ -170,11 +162,7 @@ export const createLensEmbeddableFactory = ( dashboardConfig.reinitializeState(lastSaved?.rawState); searchContextConfig.reinitializeState(lastSaved?.rawState); if (!lastSaved) return; - const lastSavedRuntimeState = await deserializeState( - services, - lastSaved.rawState, - lastSaved.references - ); + const lastSavedRuntimeState = await deserializeState(services, lastSaved.rawState); stateConfig.reinitializeRuntimeState(lastSavedRuntimeState); }, }); diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx index 7a7a31a29663e..5b8bb90c6b211 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx @@ -10,9 +10,14 @@ import { useSearchApi } from '@kbn/presentation-publishing'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { BehaviorSubject } from 'rxjs'; import type { PresentationPanelProps } from '@kbn/presentation-panel-plugin/public'; -import type { LensApi, LensRendererProps, LensSerializedState } from '@kbn/lens-common'; +import type { + LensApi, + LensParentApi, + LensRendererProps, + LensSerializedState, +} from '@kbn/lens-common'; import { LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; -import { createEmptyLensState } from '../helper'; +import { createEmptyLensState, transformOutputState } from '../helper'; // This little utility uses the same pattern of the useSearchApi hook: // create the Subject once and then update its value on change @@ -141,30 +146,31 @@ export function LensRenderer({ type={LENS_EMBEDDABLE_TYPE} maybeId={id} - // TODO type this ParentApi, all these are untyped and some unused - getParentApi={() => ({ - // forward the Lens components to the embeddable - ...props, - // forward the unified search context - ...searchApi, - searchSessionId$, - disabledActionIds$, - setDisabledActionIds: (ids: string[] | undefined) => disabledActionIds$.next(ids), - viewMode$, - // pass the sync* settings with the unified settings interface - settings, - // make sure to provide the initial state (useful for the comparison check) - getSerializedStateForChild: () => ({ rawState: initialStateRef.current, references: [] }), - // update the runtime state on changes - getRuntimeStateForChild: () => ({ - ...initialStateRef.current, - attributes: props.attributes, - }), - forceDSL, - esqlVariables$, - hideTitle$, - reload$, // trigger a reload (replacement for deprepcated searchSessionId) - })} + getParentApi={() => + ({ + // forward the Lens components to the embeddable + ...props, + // forward the unified search context + ...searchApi, + searchSessionId$, + disabledActionIds$, + setDisabledActionIds: (ids: string[] | undefined) => disabledActionIds$.next(ids), + viewMode$, + // pass the sync* settings with the unified settings interface + settings, + // make sure to provide the initial state (useful for the comparison check) + getSerializedStateForChild: () => { + const transformedState = transformOutputState(initialStateRef.current); + return { + rawState: transformedState, + }; + }, + forceDSL, + esqlVariables$, + hideTitle$, + reload$, // trigger a reload (replacement for deprecated searchSessionId) + } satisfies LensParentApi) + } onApiAvailable={setLensApi} hidePanelChrome={!showPanelChrome} panelProps={panelProps} diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/utils.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/utils.ts index ba060788f4945..44aa6909cad53 100644 --- a/x-pack/platform/plugins/shared/lens/server/api/routes/utils.ts +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/utils.ts @@ -5,41 +5,80 @@ * 2.0. */ -import type { LensResponseItem, LensSavedObject } from '../../content_management'; -import { ConfigBuilderStub } from '../../../common/transforms'; +import type { LensSavedObject, LensUpdateIn } from '../../content_management'; +import type { + LensCreateRequestBody, + LensItemMeta, + LensResponseItem, + LensUpdateRequestBody, +} from './types'; + +/** + * Converts Lens request data to Lens Config + */ +export function getLensRequestConfig( + request: LensCreateRequestBody | LensUpdateRequestBody +): LensUpdateIn['data'] & LensUpdateIn['options'] { + const { visualizationType, ...attributes } = request; + + if (!visualizationType) { + throw new Error('Missing visualizationType'); + } + + return { + ...attributes, + // TODO: fix these type issues + visualizationType, + title: attributes.title ?? '', + description: attributes.description ?? undefined, + } satisfies LensUpdateIn['data'] & LensUpdateIn['options']; +} + +/** + * Used to extend the meta of the response item. Needed in Lens GET request. + */ +export type ExtendedLensResponseItem = {}> = Omit< + LensResponseItem, + 'meta' +> & { + meta: LensResponseItem['meta'] & M; +}; /** * Converts Lens Saved Object to Lens Response Item */ -export function getLensResponseItem({ - // Data params - id, - references, - attributes, - - // Meta params - type, - createdAt, - updatedAt, - createdBy, - updatedBy, - managed, - originId, -}: LensSavedObject): LensResponseItem { +export function getLensResponseItem>( + item: LensSavedObject, + extraMeta: M = {} as M +): ExtendedLensResponseItem { + const { id, references, attributes } = item; + const meta = getLensResponseItemMeta(item, extraMeta); + return { - data: ConfigBuilderStub.out({ - ...attributes, - id, + id, + data: { references, - }), - meta: { - type, - createdAt, - updatedAt, - createdBy, - updatedBy, - managed, - originId, + ...attributes, }, + meta, + } satisfies LensResponseItem; +} + +/** + * Converts Lens Saved Object to Lens Response Item + */ +function getLensResponseItemMeta>( + { type, createdAt, updatedAt, createdBy, updatedBy, managed, originId }: LensSavedObject, + extraMeta: M = {} as M +): LensItemMeta & M { + return { + type, + createdAt, + updatedAt, + createdBy, + updatedBy, + managed, + originId, + ...extraMeta, }; } diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/create.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/create.ts index cc25dcc3ef6f4..a13e001030914 100644 --- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/create.ts +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/create.ts @@ -5,11 +5,8 @@ * 2.0. */ -import { omit } from 'lodash'; import { boomify, isBoom } from '@hapi/boom'; -import type { TypeOf } from '@kbn/config-schema'; - import { LENS_VIS_API_PATH, LENS_API_VERSION, @@ -17,18 +14,21 @@ import { LENS_CONTENT_TYPE, } from '../../../../common/constants'; import type { LensCreateIn, LensSavedObject } from '../../../content_management'; -import type { RegisterAPIRouteFn } from '../../types'; -import { ConfigBuilderStub } from '../../../../common/transforms'; -import { lensCreateRequestBodySchema, lensCreateResponseBodySchema } from './schema'; -import { getLensResponseItem } from '../utils'; -import { isNewApiFormat } from '../../../../common/transforms/config_builder_stub'; +import type { LensCreateResponseBody, RegisterAPIRouteFn } from '../../types'; +import { + lensCreateRequestBodySchema, + lensCreateRequestParamsSchema, + lensCreateRequestQuerySchema, + lensCreateResponseBodySchema, +} from './schema'; +import { getLensRequestConfig, getLensResponseItem } from '../utils'; export const registerLensVisualizationsCreateAPIRoute: RegisterAPIRouteFn = ( router, { contentManagement } ) => { const createRoute = router.post({ - path: LENS_VIS_API_PATH, + path: `${LENS_VIS_API_PATH}/{id?}`, access: LENS_API_ACCESS, enableQueryVersion: true, summary: 'Create Lens visualization', @@ -52,6 +52,8 @@ export const registerLensVisualizationsCreateAPIRoute: RegisterAPIRouteFn = ( version: LENS_API_VERSION, validate: { request: { + query: lensCreateRequestQuerySchema, + params: lensCreateRequestParamsSchema, body: lensCreateRequestBodySchema, }, response: { @@ -75,8 +77,8 @@ export const registerLensVisualizationsCreateAPIRoute: RegisterAPIRouteFn = ( }, }, async (ctx, req, res) => { - const requestBodyData = req.body.data; - if (!requestBodyData.visualizationType) { + const requestBodyData = req.body; + if ('state' in requestBodyData && !requestBodyData.visualizationType) { throw new Error('visualizationType is required'); } @@ -85,29 +87,19 @@ export const registerLensVisualizationsCreateAPIRoute: RegisterAPIRouteFn = ( .getForRequest({ request: req, requestHandlerContext: ctx }) .for(LENS_CONTENT_TYPE); - const { references, ...lensItem } = isNewApiFormat(requestBodyData) - ? // TODO: Find a better way to conditionally omit id - omit(ConfigBuilderStub.in(requestBodyData), 'id') - : // For now we need to be able to create old SO, this may be moved to the config builder - ({ - ...requestBodyData, - // fix type mismatches, null -> undefined - description: requestBodyData.description ?? undefined, - visualizationType: requestBodyData.visualizationType, - } satisfies LensCreateIn['data']); - try { // Note: these types are to enforce loose param typings of client methods - const data: LensCreateIn['data'] = lensItem; - const options: LensCreateIn['options'] = { ...req.body.options, references }; + const { references, ...data } = getLensRequestConfig(req.body); + const options: LensCreateIn['options'] = { ...req.query, references, id: req.params.id }; const { result } = await client.create(data, options); if (result.item.error) { throw result.item.error; } - return res.created>({ - body: getLensResponseItem(result.item), + const responseItem = getLensResponseItem(result.item); + return res.created({ + body: responseItem, }); } catch (error) { if (isBoom(error) && error.output.statusCode === 403) { diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/get.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/get.ts index 5ea4aee4dbc38..1fa8b23a144ac 100644 --- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/get.ts +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/get.ts @@ -16,7 +16,7 @@ import { LENS_CONTENT_TYPE, } from '../../../../common/constants'; import type { LensSavedObject } from '../../../content_management'; -import type { RegisterAPIRouteFn } from '../../types'; +import type { CMItemResultMeta, RegisterAPIRouteFn } from '../../types'; import { lensGetRequestParamsSchema, lensGetResponseBodySchema } from './schema'; import { getLensResponseItem } from '../utils'; @@ -87,15 +87,11 @@ export const registerLensVisualizationsGetAPIRoute: RegisterAPIRouteFn = ( throw result.item.error; } - const body = getLensResponseItem(result.item); + const resultMeta: CMItemResultMeta = result.meta; + const responseItem = getLensResponseItem(result.item, resultMeta); + return res.ok>({ - body: { - ...body, - meta: { - ...body.meta, - ...result.meta, - }, - }, + body: responseItem, }); } catch (error) { if (isBoom(error)) { diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/common.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/common.ts new file mode 100644 index 0000000000000..e0e5dba76a3a1 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/common.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { lensItemDataSchema, lensSavedObjectSchema } from '../../../../content_management'; +import { pickFromObjectSchema } from '../../../../utils'; + +/** + * The Lens item meta returned from the server + */ +export const lensItemMetaSchema = schema.object( + { + ...pickFromObjectSchema(lensSavedObjectSchema.getPropSchemas(), [ + 'type', + 'createdAt', + 'updatedAt', + 'createdBy', + 'updatedBy', + 'originId', + 'managed', + ]), + }, + { unknowns: 'forbid' } +); + +/** + * The Lens response item returned from the server + */ +export const lensResponseItemSchema = schema.object( + { + id: lensSavedObjectSchema.getPropSchemas().id, + data: lensItemDataSchema, + meta: lensItemMetaSchema, + }, + { unknowns: 'forbid' } +); diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/create.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/create.ts index d6974bf129ef0..ec8fa1231c4c2 100644 --- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/create.ts +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/create.ts @@ -8,36 +8,31 @@ import { schema } from '@kbn/config-schema'; import { - lensResponseItemSchema, - lensAPIConfigSchema, lensCMCreateOptionsSchema, + lensItemDataSchema, + lensItemSchema, } from '../../../../content_management'; -import { lensItemSchemaV0 } from '../../../../content_management/v0'; +import { lensItemDataSchemaV0 } from '../../../../content_management/v0'; import { pickFromObjectSchema } from '../../../../utils'; +import { lensResponseItemSchema } from './common'; -const apiConfigData = lensAPIConfigSchema.extends({ - id: undefined, -}); - -const v0ConfigData = lensItemSchemaV0.extends({ - id: undefined, -}); +export const lensCreateRequestParamsSchema = schema.object( + { + id: schema.maybe(lensItemSchema.getPropSchemas().id), + }, + { unknowns: 'forbid' } +); -export const lensCreateRequestBodySchema = schema.object( +export const lensCreateRequestQuerySchema = schema.object( { - data: schema.oneOf([ - apiConfigData, - v0ConfigData, // Temporarily permit passing old v0 SO attributes on create - ]), - // TODO should these options be here or in params? - options: schema.object( - { - ...pickFromObjectSchema(lensCMCreateOptionsSchema.getPropSchemas(), ['overwrite']), - }, - { unknowns: 'forbid' } - ), + ...pickFromObjectSchema(lensCMCreateOptionsSchema.getPropSchemas(), ['overwrite']), }, { unknowns: 'forbid' } ); +export const lensCreateRequestBodySchema = schema.oneOf([ + lensItemDataSchema, + lensItemDataSchemaV0, // Temporarily permit passing old v0 SO attributes on create +]); + export const lensCreateResponseBodySchema = lensResponseItemSchema; diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/get.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/get.ts index a65da3195fc47..476a98094c94e 100644 --- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/get.ts +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/get.ts @@ -7,7 +7,8 @@ import { schema } from '@kbn/config-schema'; -import { lensCMGetResultSchema, lensResponseItemSchema } from '../../../../content_management'; +import { lensCMGetResultSchema } from '../../../../content_management'; +import { lensResponseItemSchema } from './common'; export const lensGetRequestParamsSchema = schema.object( { @@ -22,6 +23,7 @@ export const lensGetRequestParamsSchema = schema.object( export const lensGetResponseBodySchema = schema.object( { + id: lensResponseItemSchema.getPropSchemas().id, data: lensResponseItemSchema.getPropSchemas().data, meta: schema.object( { diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/search.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/search.ts index 4db82d8646cdd..ab9d8e7f8ddec 100644 --- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/search.ts +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/search.ts @@ -8,8 +8,9 @@ import { schema } from '@kbn/config-schema'; import { searchOptionsSchemas } from '@kbn/content-management-utils'; -import { lensCMSearchOptionsSchema, lensResponseItemSchema } from '../../../../content_management'; +import { lensCMSearchOptionsSchema } from '../../../../content_management'; import { pickFromObjectSchema } from '../../../../utils'; +import { lensResponseItemSchema } from './common'; // TODO cleanup and align search options types with client side options // TODO align defaults with cm and other schema definitions (i.e. searchOptionsSchemas) diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/update.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/update.ts index 2758e5049d36c..25ed3a091fb96 100644 --- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/update.ts +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/update.ts @@ -5,16 +5,13 @@ * 2.0. */ +import { omit } from 'lodash'; + import { schema } from '@kbn/config-schema'; -import { omit } from 'lodash'; -import { - lensResponseItemSchema, - lensAPIAttributesSchema, - lensAPIConfigSchema, - lensCMUpdateOptionsSchema, -} from '../../../../content_management'; -import { pickFromObjectSchema } from '../../../../utils'; +import { lensCMUpdateOptionsSchema, lensItemDataSchema } from '../../../../content_management'; +import { lensItemDataSchemaV0 } from '../../../../content_management/v0'; +import { lensResponseItemSchema } from './common'; export const lensUpdateRequestParamsSchema = schema.object( { @@ -27,27 +24,28 @@ export const lensUpdateRequestParamsSchema = schema.object( { unknowns: 'forbid' } ); -export const lensUpdateRequestBodySchema = schema.object( +export const lensUpdateRequestQuerySchema = schema.object( { - data: schema.object( - { - ...lensAPIAttributesSchema.getPropSchemas(), - // omit id on create options - ...pickFromObjectSchema(lensAPIConfigSchema.getPropSchemas(), ['references']), - }, - { unknowns: 'forbid' } - ), - // TODO should these options be here? - options: schema.object( + ...omit(lensCMUpdateOptionsSchema.getPropSchemas(), ['references']), + }, + { unknowns: 'forbid' } +); + +export const lensUpdateRequestBodySchema = schema.oneOf([ + lensItemDataSchema, + lensItemDataSchemaV0, // Temporarily permit passing old v0 SO attributes on create +]); + +export const lensUpdateResponseBodySchema = schema.object( + { + id: lensResponseItemSchema.getPropSchemas().id, + data: lensResponseItemSchema.getPropSchemas().data, + meta: schema.object( { - ...omit(lensCMUpdateOptionsSchema.getPropSchemas(), ['references']), + ...lensResponseItemSchema.getPropSchemas().meta.getPropSchemas(), }, { unknowns: 'forbid' } ), }, - { - unknowns: 'forbid', - } + { unknowns: 'forbid' } ); - -export const lensUpdateResponseBodySchema = lensResponseItemSchema; diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/types.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/types.ts index 31b1e825c3275..99cef968846ff 100644 --- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/types.ts +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/types.ts @@ -5,11 +5,14 @@ * 2.0. */ +import type { Optional } from 'utility-types'; + import type { TypeOf } from '@kbn/config-schema'; -import type { Optional } from 'utility-types'; +import type { lensCMGetResultSchema } from '../../../content_management'; import type { lensCreateRequestBodySchema, + lensCreateRequestQuerySchema, lensCreateResponseBodySchema, lensDeleteRequestParamsSchema, lensGetRequestParamsSchema, @@ -18,13 +21,21 @@ import type { lensSearchResponseBodySchema, lensUpdateRequestBodySchema, lensUpdateRequestParamsSchema, + lensUpdateRequestQuerySchema, lensUpdateResponseBodySchema, } from './schema'; +import type { lensItemMetaSchema, lensResponseItemSchema } from './schema/common'; + +export type LensItemMeta = TypeOf; +export type LensResponseItem = TypeOf; +export type CMItemResultMeta = TypeOf['meta']; +export type LensCreateRequestQuery = TypeOf; export type LensCreateRequestBody = TypeOf; export type LensCreateResponseBody = TypeOf; export type LensUpdateRequestParams = TypeOf; +export type LensUpdateRequestQuery = TypeOf; export type LensUpdateRequestBody = TypeOf; export type LensUpdateResponseBody = TypeOf; diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/update.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/update.ts index d5bab1a71bf37..e12f91e07af26 100644 --- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/update.ts +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/update.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { omit } from 'lodash'; import { boomify, isBoom } from '@hapi/boom'; -import type { TypeOf } from '@kbn/config-schema'; import { LENS_VIS_API_PATH, @@ -16,14 +14,14 @@ import { LENS_CONTENT_TYPE, } from '../../../../common/constants'; import type { LensUpdateIn, LensSavedObject } from '../../../content_management'; -import type { RegisterAPIRouteFn } from '../../types'; -import { ConfigBuilderStub } from '../../../../common/transforms'; +import type { LensUpdateResponseBody, RegisterAPIRouteFn } from '../../types'; import { lensUpdateRequestBodySchema, lensUpdateRequestParamsSchema, + lensUpdateRequestQuerySchema, lensUpdateResponseBodySchema, } from './schema'; -import { getLensResponseItem } from '../utils'; +import { getLensRequestConfig, getLensResponseItem } from '../utils'; export const registerLensVisualizationsUpdateAPIRoute: RegisterAPIRouteFn = ( router, @@ -56,6 +54,7 @@ export const registerLensVisualizationsUpdateAPIRoute: RegisterAPIRouteFn = ( request: { params: lensUpdateRequestParamsSchema, body: lensUpdateRequestBodySchema, + query: lensUpdateRequestQuerySchema, }, response: { 200: { @@ -81,7 +80,7 @@ export const registerLensVisualizationsUpdateAPIRoute: RegisterAPIRouteFn = ( }, }, async (ctx, req, res) => { - const requestBodyData = req.body.data; + const requestBodyData = req.body; if (!requestBodyData.visualizationType) { throw new Error('visualizationType is required'); } @@ -91,26 +90,20 @@ export const registerLensVisualizationsUpdateAPIRoute: RegisterAPIRouteFn = ( .getForRequest({ request: req, requestHandlerContext: ctx }) .for(LENS_CONTENT_TYPE); - const { references, ...lensItem } = omit( - ConfigBuilderStub.in({ - id: '', // TODO: Find a better way to conditionally omit id - ...req.body.data, - }), - 'id' - ); + // Note: these types are to enforce loose param typings of client methods + const { references, ...data } = getLensRequestConfig(req.body); + const options: LensUpdateIn['options'] = { ...req.query, references }; try { - // Note: these types are to enforce loose param typings of client methods - const data: LensUpdateIn['data'] = lensItem; - const options: LensUpdateIn['options'] = { references }; const { result } = await client.update(req.params.id, data, options); if (result.item.error) { throw result.item.error; } - return res.ok>({ - body: getLensResponseItem(result.item), + const responseItem = getLensResponseItem(result.item); + return res.ok({ + body: responseItem, }); } catch (error) { if (isBoom(error)) { diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v0/schema/common.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v0/schema/common.ts index 5df9f7232d1d7..874fd5781fd45 100644 --- a/x-pack/platform/plugins/shared/lens/server/content_management/v0/schema/common.ts +++ b/x-pack/platform/plugins/shared/lens/server/content_management/v0/schema/common.ts @@ -52,3 +52,7 @@ export const lensItemSchemaV0 = schema.object( }, { unknowns: 'forbid' } ); + +export const lensItemDataSchemaV0 = lensItemSchemaV0.extends({ + id: undefined, +}); diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/common.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/common.ts index 28fb9099e3ee2..1b96f0f5dcc97 100644 --- a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/common.ts +++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/common.ts @@ -23,21 +23,6 @@ export const lensItemAttributesSchema = schema.object( { unknowns: 'forbid' } ); -export const lensAPIStateSchema = schema.object( - { - isNewApiFormat: schema.literal(true), // pin this to validate CB transformations - }, - { unknowns: 'allow' } -); - -export const lensAPIAttributesSchema = schema.object( - { - ...lensItemAttributesSchema.getPropSchemas(), - state: lensAPIStateSchema, - }, - { unknowns: 'forbid' } -); - /** * The underlying SO type used to store Lens state in Content Management. * @@ -45,68 +30,30 @@ export const lensAPIAttributesSchema = schema.object( */ export const lensSavedObjectSchema = savedObjectSchema(lensItemAttributesSchema); -/** - * The common SO type used for mSearch items. - */ -export const lensCommonSavedObjectSchema = savedObjectSchema( - schema.object( - { - ...pickFromObjectSchema(lensItemAttributesSchema.getPropSchemas(), ['title', 'description']), - }, - { unknowns: 'forbid' } - ) -); - /** * The Lens item data returned from the server */ export const lensItemSchema = schema.object( { ...pickFromObjectSchema(lensSavedObjectSchema.getPropSchemas(), ['id', 'references']), - // Spread attributes at root ...lensSavedObjectSchema.getPropSchemas().attributes.getPropSchemas(), }, { unknowns: 'forbid' } ); /** - * The Lens item data returned from the server - */ -export const lensAPIConfigSchema = schema.object( - { - // TODO flatten this with new CB shape - ...pickFromObjectSchema(lensSavedObjectSchema.getPropSchemas(), ['id', 'references']), - // Spread attributes at root - ...lensAPIAttributesSchema.getPropSchemas(), - }, - { unknowns: 'forbid' } -); - -/** - * The Lens item meta returned from the server + * The common SO type used for mSearch items. */ -export const lensItemMetaSchema = schema.object( - { - ...pickFromObjectSchema(lensSavedObjectSchema.getPropSchemas(), [ - 'type', - 'createdAt', - 'updatedAt', - 'createdBy', - 'updatedBy', - 'originId', // maybe?? - 'managed', - ]), - }, - { unknowns: 'forbid' } +export const lensCommonSavedObjectSchema = savedObjectSchema( + schema.object( + { + ...pickFromObjectSchema(lensItemAttributesSchema.getPropSchemas(), ['title', 'description']), + }, + { unknowns: 'forbid' } + ) ); -/** - * The Lens response item returned from the server - */ -export const lensResponseItemSchema = schema.object( - { - data: lensAPIConfigSchema, - meta: lensItemMetaSchema, - }, - { unknowns: 'forbid' } -); +// TODO: cleanup data for update, should we forbid or just ignore body.id on update? +export const lensItemDataSchema = lensItemSchema.extends({ + id: undefined, +}); diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/create.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/create.ts index 512edba1b5c52..6f0f604e7c04e 100644 --- a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/create.ts +++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/create.ts @@ -14,7 +14,7 @@ import { pickFromObjectSchema } from '../../../utils'; export const lensCMCreateOptionsSchema = schema.object( { - ...pickFromObjectSchema(createOptionsSchemas, ['overwrite', 'references']), + ...pickFromObjectSchema(createOptionsSchemas, ['id', 'overwrite', 'references']), }, { unknowns: 'forbid' } ); diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/types.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/types.ts index 90889c8b9e0ab..a32eb023b51ac 100644 --- a/x-pack/platform/plugins/shared/lens/server/content_management/v1/types.ts +++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/types.ts @@ -29,21 +29,11 @@ import type { lensCMUpdateOptionsSchema, lensCMSearchOptionsSchema, lensItemSchema, - lensItemMetaSchema, - lensResponseItemSchema, - lensAPIAttributesSchema, - lensAPIConfigSchema, } from './schema'; import type { LENS_CONTENT_TYPE } from '../../../common/constants'; export type LensAttributes = TypeOf; -export type LensAPIAttributes = TypeOf; - export type LensItem = TypeOf; -export type LensItemMeta = TypeOf; - -export type LensAPIConfig = TypeOf; -export type LensResponseItem = TypeOf; export type LensCreateOptions = TypeOf; export type LensUpdateOptions = TypeOf; diff --git a/x-pack/platform/plugins/shared/lens/server/embeddable/make_lens_embeddable_factory.ts b/x-pack/platform/plugins/shared/lens/server/embeddable/make_lens_embeddable_factory.ts index e6f0ca054b23d..3a82689d2abfd 100644 --- a/x-pack/platform/plugins/shared/lens/server/embeddable/make_lens_embeddable_factory.ts +++ b/x-pack/platform/plugins/shared/lens/server/embeddable/make_lens_embeddable_factory.ts @@ -5,11 +5,12 @@ * 2.0. */ -import type { EmbeddableRegistryDefinition } from '@kbn/embeddable-plugin/server'; import type { SerializableRecord } from '@kbn/utility-types'; import type { SavedObject } from '@kbn/core-saved-objects-server'; import type { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common'; import { mergeMigrationFunctionMaps } from '@kbn/kibana-utils-plugin/common'; + +import { inject, extract } from './references'; import { DOC_TYPE } from '../../common/constants'; import { commonEnhanceTableRowHeight, @@ -49,7 +50,7 @@ import type { VisStatePre830, XYVisState850, } from '../migrations/types'; -import { extract, inject } from '../../common/embeddable_factory'; +import type { LensEmbeddableRegistryDefinition } from './types'; export const makeLensEmbeddableFactory = ( @@ -57,7 +58,7 @@ export const makeLensEmbeddableFactory = getDataViewMigrations: () => MigrateFunctionsObject, customVisualizationMigrations: CustomVisualizationMigrations ) => - (): EmbeddableRegistryDefinition => { + (): LensEmbeddableRegistryDefinition => { return { id: DOC_TYPE, migrations: () => diff --git a/x-pack/platform/plugins/shared/lens/server/embeddable/references.ts b/x-pack/platform/plugins/shared/lens/server/embeddable/references.ts new file mode 100644 index 0000000000000..68fc93fbc7abc --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/embeddable/references.ts @@ -0,0 +1,27 @@ +/* + * 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 { extractLensReferences, injectLensReferences } from '../../common/references'; +import type { LensEmbeddableRegistryDefinition } from './types'; + +export const inject: LensEmbeddableRegistryDefinition['inject'] = (state, references) => { + return { + ...state, + ...injectLensReferences(state, references), + }; +}; + +export const extract: LensEmbeddableRegistryDefinition['extract'] = (state) => { + const { state: newState, references } = extractLensReferences(state); + return { + state: { + ...state, + ...newState, + }, + references, + }; +}; diff --git a/x-pack/platform/plugins/shared/lens/server/embeddable/types.ts b/x-pack/platform/plugins/shared/lens/server/embeddable/types.ts new file mode 100644 index 0000000000000..a63f4ad0aa580 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/embeddable/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + EmbeddableRegistryDefinition, + EmbeddableStateWithType, +} from '@kbn/embeddable-plugin/server'; + +import type { LensRuntimeState } from '../../public'; +import type { DOC_TYPE } from '../../common/constants'; + +export type LensEmbeddableStateWithType = EmbeddableStateWithType & + LensRuntimeState & { + type: typeof DOC_TYPE; + }; + +export type LensEmbeddableRegistryDefinition = + EmbeddableRegistryDefinition; diff --git a/x-pack/platform/plugins/shared/lens/server/plugin.tsx b/x-pack/platform/plugins/shared/lens/server/plugin.tsx index 479b6fa473150..fbfe58a05e2e9 100644 --- a/x-pack/platform/plugins/shared/lens/server/plugin.tsx +++ b/x-pack/platform/plugins/shared/lens/server/plugin.tsx @@ -26,7 +26,7 @@ import type { TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; -import type { EmbeddableSetup } from '@kbn/embeddable-plugin/server'; +import type { EmbeddableRegistryDefinition, EmbeddableSetup } from '@kbn/embeddable-plugin/server'; import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; import type { SharePluginSetup } from '@kbn/share-plugin/server'; import { setupSavedObjects } from './saved_objects'; @@ -34,9 +34,14 @@ import { setupExpressions } from './expressions'; import { makeLensEmbeddableFactory } from './embeddable/make_lens_embeddable_factory'; import type { CustomVisualizationMigrations } from './migrations/types'; import { LensAppLocatorDefinition } from '../common/locator/locator'; -import { LENS_CONTENT_TYPE, LENS_ITEM_LATEST_VERSION } from '../common/constants'; +import { + LENS_CONTENT_TYPE, + LENS_EMBEDDABLE_TYPE, + LENS_ITEM_LATEST_VERSION, +} from '../common/constants'; import { LensStorage } from './content_management'; import { registerLensAPIRoutes } from './api/routes'; +import { getLensTransforms } from '../common/transforms'; export interface PluginSetupContract { taskManager?: TaskManagerSetupContract; @@ -105,7 +110,18 @@ export class LensServerPlugin DataViewPersistableStateService.getAllMigrations.bind(DataViewPersistableStateService), this.customVisualizationMigrations ); - plugins.embeddable.registerEmbeddableFactory(lensEmbeddableFactory()); + + plugins.embeddable.registerEmbeddableFactory( + lensEmbeddableFactory() as unknown as EmbeddableRegistryDefinition + ); + + plugins.embeddable.registerTransforms( + LENS_EMBEDDABLE_TYPE, + getLensTransforms({ + transformEnhancementsIn: plugins.embeddable.transformEnhancementsIn, + transformEnhancementsOut: plugins.embeddable.transformEnhancementsOut, + }) + ); registerLensAPIRoutes({ http: core.http, diff --git a/x-pack/platform/test/api_integration/apis/lens/examples.ts b/x-pack/platform/test/api_integration/apis/lens/examples.ts index 9a0ac294ba786..e129f5c43a25b 100644 --- a/x-pack/platform/test/api_integration/apis/lens/examples.ts +++ b/x-pack/platform/test/api_integration/apis/lens/examples.ts @@ -11,69 +11,66 @@ export const getExampleLensBody = ( title = `Lens vis - ${Date.now()} - ${Math.random()}`, description = '' ): LensCreateRequestBody => ({ - data: { - title, - description, - visualizationType: 'lnsMetric', - state: { - visualization: { - layerId: '32e889c6-89f9-4873-b1f7-d5bea381c582', - layerType: 'data', - metricAccessor: '1c6729bc-ec92-4000-8dcc-0fdd7b56d5b8', - secondaryTrend: { - type: 'none', - }, - }, - query: { - query: '', - language: 'kuery', + title, + description, + visualizationType: 'lnsMetric', + state: { + visualization: { + layerId: '32e889c6-89f9-4873-b1f7-d5bea381c582', + layerType: 'data', + metricAccessor: '1c6729bc-ec92-4000-8dcc-0fdd7b56d5b8', + secondaryTrend: { + type: 'none', }, - filters: [], - datasourceStates: { - formBased: { - layers: { - '32e889c6-89f9-4873-b1f7-d5bea381c582': { - columns: { - '1c6729bc-ec92-4000-8dcc-0fdd7b56d5b8': { - label: 'Count of records', - dataType: 'number', - operationType: 'count', - isBucketed: false, - scale: 'ratio', - sourceField: '___records___', - params: { - emptyAsNull: true, - }, + }, + query: { + query: '', + language: 'kuery', + }, + filters: [], + datasourceStates: { + formBased: { + layers: { + '32e889c6-89f9-4873-b1f7-d5bea381c582': { + columns: { + '1c6729bc-ec92-4000-8dcc-0fdd7b56d5b8': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: '___records___', + params: { + emptyAsNull: true, }, }, - columnOrder: ['1c6729bc-ec92-4000-8dcc-0fdd7b56d5b8'], - incompleteColumns: { - 'd0b92889-f74c-4194-b738-76eb5d268524': { - operationType: 'date_histogram', - }, + }, + columnOrder: ['1c6729bc-ec92-4000-8dcc-0fdd7b56d5b8'], + incompleteColumns: { + 'd0b92889-f74c-4194-b738-76eb5d268524': { + operationType: 'date_histogram', }, - sampling: 1, }, + sampling: 1, }, }, - indexpattern: { - layers: {}, - }, - textBased: { - layers: {}, - }, }, - internalReferences: [], - adHocDataViews: {}, - isNewApiFormat: true, // temporary flag - }, - references: [ - { - type: 'index-pattern', - id: '91200a00-9efd-11e7-acb3-3dab96693fab', - name: 'indexpattern-datasource-layer-32e889c6-89f9-4873-b1f7-d5bea381c582', + indexpattern: { + layers: {}, }, - ], + textBased: { + layers: {}, + }, + }, + internalReferences: [], + adHocDataViews: {}, + isNewApiFormat: true, // temporary flag }, - options: {}, + references: [ + { + type: 'index-pattern', + id: '91200a00-9efd-11e7-acb3-3dab96693fab', + name: 'indexpattern-datasource-layer-32e889c6-89f9-4873-b1f7-d5bea381c582', + }, + ], }); diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/create/validation.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/create/validation.ts index 19e58e58b1e08..de4d1f305dcca 100644 --- a/x-pack/platform/test/api_integration/apis/lens/visualizations/create/validation.ts +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/create/validation.ts @@ -23,7 +23,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.status).to.be(400); expect(response.body.message).to.be( - '[request body.data]: expected at least one defined value but got [undefined]' + '[request body]: types that failed validation:\n- [request body.0.references]: expected value of type [array] but got [undefined]\n- [request body.1.references]: expected value of type [array] but got [undefined]' ); }); }); diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/update/validation.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/update/validation.ts index e1c4c0e269397..d1c6a420a1be4 100644 --- a/x-pack/platform/test/api_integration/apis/lens/visualizations/update/validation.ts +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/update/validation.ts @@ -24,7 +24,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.status).to.be(400); expect(response.body.message).to.be( - '[request body.data.title]: expected value of type [string] but got [undefined]' + '[request body]: types that failed validation:\n- [request body.0.references]: expected value of type [array] but got [undefined]\n- [request body.1.references]: expected value of type [array] but got [undefined]' ); }); });