[Serialized state only] Update Library Transforms and Duplicate (#206140)

Unifies the various `LibraryTransforms` interfaces, updates all by reference capable embeddables to use them in the same way, and migrates the clone functionality to use only serialized state.
This commit is contained in:
Devon Thomson 2025-01-21 13:43:43 -05:00 committed by GitHub
parent 8ff18e2575
commit 3719be0144
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 487 additions and 915 deletions

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { SerializedPanelState } from '@kbn/presentation-containers';
import type { SerializedPanelState } from '@kbn/presentation-publishing';
import type { ControlGroupSerializedState } from '@kbn/controls-plugin/common';
import {
OPTIONS_LIST_CONTROL,

View file

@ -12,7 +12,6 @@ import { v4 as generateId } from 'uuid';
import { TimeRange } from '@kbn/es-query';
import {
PanelPackage,
apiHasSerializableState,
childrenUnsavedChanges$,
combineCompatibleChildrenApis,
} from '@kbn/presentation-containers';
@ -21,6 +20,7 @@ import {
PublishesDataLoading,
PublishingSubject,
ViewMode,
apiHasSerializableState,
apiPublishesDataLoading,
apiPublishesUnsavedChanges,
} from '@kbn/presentation-publishing';

View file

@ -13,7 +13,6 @@ import {
HasSerializedChildState,
HasRuntimeChildState,
PresentationContainer,
SerializedPanelState,
HasSaveNotification,
} from '@kbn/presentation-containers';
import {
@ -21,6 +20,7 @@ import {
PublishesDataLoading,
PublishesTimeRange,
PublishesUnsavedChanges,
SerializedPanelState,
PublishesViewMode,
} from '@kbn/presentation-publishing';
import { PublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/publishes_reload';

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { SerializedPanelState } from '@kbn/presentation-containers';
import { SerializedPanelState } from '@kbn/presentation-publishing';
import { BookSerializedState } from '../../react_embeddables/saved_book/types';
const SAVED_STATE_SESSION_STORAGE_KEY =

View file

@ -26,11 +26,7 @@ import { CoreStart } from '@kbn/core-lifecycle-browser';
import { OverlayRef } from '@kbn/core-mount-utils-browser';
import { i18n } from '@kbn/i18n';
import { tracksOverlays } from '@kbn/presentation-containers';
import {
apiHasInPlaceLibraryTransforms,
apiHasUniqueId,
useBatchedOptionalPublishingSubjects,
} from '@kbn/presentation-publishing';
import { apiHasUniqueId, useBatchedOptionalPublishingSubjects } from '@kbn/presentation-publishing';
import { toMountPoint } from '@kbn/react-kibana-mount';
import React, { useState } from 'react';
import { serializeBookAttributes } from './book_state';
@ -74,7 +70,7 @@ export const openSavedBookEditor = ({
onSubmit={async (addToLibrary: boolean) => {
const savedBookId = addToLibrary
? await saveBookAttributes(
apiHasInPlaceLibraryTransforms(api) ? api.libraryId$.value : undefined,
api?.getSavedBookId(),
serializeBookAttributes(attributesManager)
)
: undefined;
@ -114,15 +110,13 @@ export const SavedBookEditor = ({
onCancel: () => void;
api?: BookApi;
}) => {
const [libraryId, authorName, synopsis, bookTitle, numberOfPages] =
useBatchedOptionalPublishingSubjects(
api?.libraryId$,
attributesManager.authorName,
attributesManager.bookSynopsis,
attributesManager.bookTitle,
attributesManager.numberOfPages
);
const [addToLibrary, setAddToLibrary] = useState(Boolean(libraryId));
const [authorName, synopsis, bookTitle, numberOfPages] = useBatchedOptionalPublishingSubjects(
attributesManager.authorName,
attributesManager.bookSynopsis,
attributesManager.bookTitle,
attributesManager.numberOfPages
);
const [addToLibrary, setAddToLibrary] = useState(Boolean(api?.getSavedBookId()));
const [saving, setSaving] = useState(false);
return (

View file

@ -22,12 +22,13 @@ import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import {
apiHasParentApi,
getUnchangingComparator,
initializeTitles,
SerializedTitles,
SerializedPanelState,
useBatchedPublishingSubjects,
} from '@kbn/presentation-publishing';
import React from 'react';
import { BehaviorSubject } from 'rxjs';
import { PresentationContainer } from '@kbn/presentation-containers';
import { serializeBookAttributes, stateManagerFromAttributes } from './book_state';
import { SAVED_BOOK_ID } from './constants';
@ -82,7 +83,24 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => {
buildEmbeddable: async (state, buildApi) => {
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
const bookAttributesManager = stateManagerFromAttributes(state);
const savedBookId$ = new BehaviorSubject(state.savedBookId);
const isByReference = Boolean(state.savedBookId);
const serializeBook = (byReference: boolean, newId?: string) => {
if (byReference) {
// if this book is currently by reference, we serialize the reference only.
const bookByReferenceState: BookByReferenceSerializedState = {
savedBookId: newId ?? state.savedBookId!,
...serializeTitles(),
};
return { rawState: bookByReferenceState };
}
// if this book is currently by value, we serialize the entire state.
const bookByValueState: BookByValueSerializedState = {
attributes: serializeBookAttributes(bookAttributesManager),
...serializeTitles(),
};
return { rawState: bookByValueState };
};
const api = buildApi(
{
@ -95,7 +113,15 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => {
core,
api,
}).then((result) => {
savedBookId$.next(result.savedBookId);
const nextIsByReference = Boolean(result.savedBookId);
// if the by reference state has changed during this edit, reinitialize the panel.
if (nextIsByReference !== isByReference) {
api.parentApi?.replacePanel<BookSerializedState>(api.uuid, {
serializedState: serializeBook(nextIsByReference, result.savedBookId),
panelType: api.type,
});
}
});
},
isEditingEnabled: () => true,
@ -103,47 +129,28 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => {
i18n.translate('embeddableExamples.savedbook.editBook.displayName', {
defaultMessage: 'book',
}),
serializeState: () => {
if (!Boolean(savedBookId$.value)) {
// if this book is currently by value, we serialize the entire state.
const bookByValueState: BookByValueSerializedState = {
attributes: serializeBookAttributes(bookAttributesManager),
...serializeTitles(),
};
return { rawState: bookByValueState };
}
serializeState: () => serializeBook(isByReference),
// if this book is currently by reference, we serialize the reference only.
const bookByReferenceState: BookByReferenceSerializedState = {
savedBookId: savedBookId$.value!,
...serializeTitles(),
};
return { rawState: bookByReferenceState };
},
// in place library transforms
libraryId$: savedBookId$,
// library transforms
getSavedBookId: () => state.savedBookId,
saveToLibrary: async (newTitle: string) => {
bookAttributesManager.bookTitle.next(newTitle);
const newId = await saveBookAttributes(
undefined,
serializeBookAttributes(bookAttributesManager)
);
savedBookId$.next(newId);
return newId;
},
checkForDuplicateTitle: async (title) => {},
unlinkFromLibrary: () => {
savedBookId$.next(undefined);
},
getByValueRuntimeSnapshot: () => {
const snapshot = api.snapshotRuntimeState();
delete snapshot.savedBookId;
return snapshot;
},
getSerializedStateByValue: () =>
serializeBook(false) as SerializedPanelState<BookByValueSerializedState>,
getSerializedStateByReference: (newId) =>
serializeBook(true, newId) as SerializedPanelState<BookByReferenceSerializedState>,
canLinkToLibrary: async () => !isByReference,
canUnlinkFromLibrary: async () => isByReference,
},
{
savedBookId: [savedBookId$, (val) => savedBookId$.next(val)],
savedBookId: getUnchangingComparator(), // saved book id will not change over the lifetime of the embeddable.
...bookAttributesManager.comparators,
...titleComparators,
}
@ -156,14 +163,12 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => {
return {
api,
Component: () => {
const [authorName, numberOfPages, savedBookId, bookTitle, synopsis] =
useBatchedPublishingSubjects(
bookAttributesManager.authorName,
bookAttributesManager.numberOfPages,
savedBookId$,
bookAttributesManager.bookTitle,
bookAttributesManager.bookSynopsis
);
const [authorName, numberOfPages, bookTitle, synopsis] = useBatchedPublishingSubjects(
bookAttributesManager.authorName,
bookAttributesManager.numberOfPages,
bookAttributesManager.bookTitle,
bookAttributesManager.bookSynopsis
);
const { euiTheme } = useEuiTheme();
return (
@ -177,7 +182,7 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => {
size="s"
color={'warning'}
title={
savedBookId
isByReference
? i18n.translate('embeddableExamples.savedBook.libraryCallout', {
defaultMessage: 'Saved in library',
})
@ -185,7 +190,7 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => {
defaultMessage: 'Not saved in library',
})
}
iconType={savedBookId ? 'folderCheck' : 'folderClosed'}
iconType={isByReference ? 'folderCheck' : 'folderClosed'}
/>
)}
<div

View file

@ -10,7 +10,7 @@
import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
import {
HasEditCapabilities,
HasInPlaceLibraryTransforms,
HasLibraryTransforms,
SerializedTitles,
StateComparators,
} from '@kbn/presentation-publishing';
@ -35,6 +35,10 @@ export interface BookByReferenceSerializedState {
savedBookId: string;
}
export interface HasSavedBookId {
getSavedBookId: () => string | undefined;
}
export type BookSerializedState = SerializedTitles &
(BookByValueSerializedState | BookByReferenceSerializedState);
@ -48,4 +52,5 @@ export interface BookRuntimeState
export type BookApi = DefaultEmbeddableApi<BookSerializedState, BookRuntimeState> &
HasEditCapabilities &
HasInPlaceLibraryTransforms<BookRuntimeState>;
HasLibraryTransforms<BookByReferenceSerializedState, BookByValueSerializedState> &
HasSavedBookId;

View file

@ -15,7 +15,7 @@ import type {
SearchEmbeddableRuntimeState,
SearchEmbeddableApi,
} from '@kbn/discover-plugin/public';
import { SerializedPanelState } from '@kbn/presentation-containers';
import { SerializedPanelState } from '@kbn/presentation-publishing';
import { css } from '@emotion/react';
import { SavedSearchComponentProps } from '../types';
import { SavedSearchComponentErrorContent } from './error';

View file

@ -23,7 +23,7 @@
"@kbn/es-query",
"@kbn/data-plugin",
"@kbn/discover-plugin",
"@kbn/presentation-containers",
"@kbn/i18n",
"@kbn/presentation-publishing",
]
}

View file

@ -39,10 +39,4 @@ export {
type PresentationContainer,
} from './interfaces/presentation_container';
export { apiPublishesSettings, type PublishesSettings } from './interfaces/publishes_settings';
export {
apiHasSerializableState,
type HasSerializableState,
type HasSnapshottableState,
type SerializedPanelState,
} from './interfaces/serialized_state';
export { tracksOverlays, type TracksOverlays } from './interfaces/tracks_overlays';

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { SerializedPanelState } from './serialized_state';
import { SerializedPanelState } from '@kbn/presentation-publishing';
export interface HasSerializedChildState<SerializedState extends object = object> {
getSerializedStateForChild: (
@ -15,6 +15,9 @@ export interface HasSerializedChildState<SerializedState extends object = object
) => SerializedPanelState<SerializedState> | undefined;
}
/**
* @deprecated Use `HasSerializedChildState` instead. All interactions between the container and the child should use the serialized state.
*/
export interface HasRuntimeChildState<RuntimeState extends object = object> {
getRuntimeStateForChild: (childId: string) => Partial<RuntimeState> | undefined;
}
@ -24,7 +27,9 @@ export const apiHasSerializedChildState = <SerializedState extends object = obje
): api is HasSerializedChildState<SerializedState> => {
return Boolean(api && (api as HasSerializedChildState).getSerializedStateForChild);
};
/**
* @deprecated Use `HasSerializedChildState` instead. All interactions between the container and the child should use the serialized state.
*/
export const apiHasRuntimeChildState = <RuntimeState extends object = object>(
api: unknown
): api is HasRuntimeChildState<RuntimeState> => {

View file

@ -7,13 +7,30 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { apiHasParentApi, apiHasUniqueId, PublishingSubject } from '@kbn/presentation-publishing';
import {
apiHasParentApi,
apiHasUniqueId,
PublishingSubject,
SerializedPanelState,
} from '@kbn/presentation-publishing';
import { BehaviorSubject, combineLatest, isObservable, map, Observable, of, switchMap } from 'rxjs';
import { apiCanAddNewPanel, CanAddNewPanel } from './can_add_new_panel';
export interface PanelPackage<SerializedState extends object = object> {
export interface PanelPackage<
SerializedStateType extends object = object,
RuntimeStateType extends object = object
> {
panelType: string;
initialState?: SerializedState;
/**
* The serialized state of this panel.
*/
serializedState?: SerializedPanelState<SerializedStateType>;
/**
* The runtime state of this panel. @deprecated Use `serializedState` instead.
*/
initialState?: RuntimeStateType;
}
export interface PresentationContainer extends CanAddNewPanel {

View file

@ -21,8 +21,8 @@ import {
PublishingSubject,
runComparators,
StateComparators,
HasSnapshottableState,
} from '@kbn/presentation-publishing';
import { HasSnapshottableState } from '../serialized_state';
import { apiHasSaveNotification } from '../has_save_notification';
export const COMPARATOR_SUBJECTS_DEBOUNCE = 100;

View file

@ -9,6 +9,5 @@
"kbn_references": [
"@kbn/presentation-publishing",
"@kbn/core-mount-utils-browser",
"@kbn/content-management-utils",
]
}

View file

@ -64,14 +64,17 @@ export {
type HasExecutionContext,
} from './interfaces/has_execution_context';
export {
apiHasInPlaceLibraryTransforms,
apiHasLegacyLibraryTransforms,
apiHasLibraryTransforms,
type HasInPlaceLibraryTransforms,
type HasLegacyLibraryTransforms,
type HasLibraryTransforms,
} from './interfaces/has_library_transforms';
export { apiHasParentApi, type HasParentApi } from './interfaces/has_parent_api';
export {
apiHasSerializableState,
apiHasSnapshottableState,
type HasSerializableState,
type HasSnapshottableState,
type SerializedPanelState,
} from './interfaces/has_serializable_state';
export {
apiHasSupportedTriggers,
type HasSupportedTriggers,

View file

@ -7,101 +7,57 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { PublishingSubject } from '../publishing_subject';
import { SerializedPanelState } from './has_serializable_state';
interface DuplicateTitleCheck {
/**
* APIs that inherit this interface can be linked to and unlinked from the library.
*/
export interface HasLibraryTransforms<
ByReferenceSerializedState extends object = object,
ByValueSerializedState extends object = object
> {
checkForDuplicateTitle: (
newTitle: string,
isTitleDuplicateConfirmed: boolean,
onTitleDuplicate: () => void
) => Promise<void>;
}
interface LibraryTransformGuards {
/**
*
* @returns {Promise<boolean>}
* True when embeddable is by-value and can be converted to by-reference
* Returns true when this API is by-value and can be converted to by-reference
*/
canLinkToLibrary: () => Promise<boolean>;
/**
*
* @returns {Promise<boolean>}
* True when embeddable is by-reference and can be converted to by-value
* Returns true when this API is by-reference and can be converted to by-value
*/
canUnlinkFromLibrary: () => Promise<boolean>;
}
/**
* APIs that inherit this interface can be linked to and unlinked from the library in place without
* re-initialization.
*/
export interface HasInPlaceLibraryTransforms<RuntimeState extends object = object>
extends Partial<LibraryTransformGuards>,
DuplicateTitleCheck {
/**
* The id of the library item that this embeddable is linked to.
*/
libraryId$: PublishingSubject<string | undefined>;
/**
* Save embeddable to library
* Save the state of this API to the library. This will return the ID of the persisted library item.
*
* @returns {Promise<string>} id of persisted library item
*/
saveToLibrary: (title: string) => Promise<string>;
/**
* gets a snapshot of this embeddable's runtime state without any state that links it to a library item.
*
* @returns {ByReferenceSerializedState}
* get by-reference serialized state from this API.
*/
getByValueRuntimeSnapshot: () => RuntimeState;
getSerializedStateByReference: (
newId: string
) => SerializedPanelState<ByReferenceSerializedState>;
/**
* Un-links this embeddable from the library. This method is optional, and only needed if the Embeddable
* is not meant to be re-initialized as part of the unlink operation. If the embeddable needs to be re-initialized
* after unlinking, the getByValueState method should be used instead.
*/
unlinkFromLibrary: () => void;
}
export const apiHasInPlaceLibraryTransforms = (
unknownApi: null | unknown
): unknownApi is HasInPlaceLibraryTransforms => {
return Boolean(
unknownApi &&
Boolean((unknownApi as HasInPlaceLibraryTransforms)?.libraryId$) &&
typeof (unknownApi as HasInPlaceLibraryTransforms).saveToLibrary === 'function' &&
typeof (unknownApi as HasInPlaceLibraryTransforms).unlinkFromLibrary === 'function'
);
};
/**
* @deprecated use HasInPlaceLibraryTransforms instead
* APIs that inherit this interface can be linked to and unlinked from the library. After the save or unlink
* operation, the embeddable will be reinitialized.
*/
export interface HasLibraryTransforms<StateT extends object = object>
extends LibraryTransformGuards,
DuplicateTitleCheck {
/**
* Save embeddable to library
*
* @returns {Promise<string>} id of persisted library item
* @returns {ByValueSerializedState}
* get by-value serialized state from this API
*/
saveToLibrary: (title: string) => Promise<string>;
/**
*
* @returns {StateT}
* by-reference embeddable state replacing by-value embeddable state. After
* the save operation, the embeddable will be reinitialized with the results of this method.
*/
getByReferenceState: (libraryId: string) => StateT;
/**
*
* @returns {StateT}
* by-value embeddable state replacing by-reference embeddable state. After
* the unlink operation, the embeddable will be reinitialized with the results of this method.
*/
getByValueState: () => StateT;
getSerializedStateByValue: () => SerializedPanelState<ByValueSerializedState>;
}
export const apiHasLibraryTransforms = <StateT extends object = object>(
@ -112,34 +68,10 @@ export const apiHasLibraryTransforms = <StateT extends object = object>(
typeof (unknownApi as HasLibraryTransforms<StateT>).canLinkToLibrary === 'function' &&
typeof (unknownApi as HasLibraryTransforms<StateT>).canUnlinkFromLibrary === 'function' &&
typeof (unknownApi as HasLibraryTransforms<StateT>).saveToLibrary === 'function' &&
typeof (unknownApi as HasLibraryTransforms<StateT>).getByReferenceState === 'function' &&
typeof (unknownApi as HasLibraryTransforms<StateT>).getByValueState === 'function' &&
typeof (unknownApi as HasLibraryTransforms<StateT>).getSerializedStateByReference ===
'function' &&
typeof (unknownApi as HasLibraryTransforms<StateT>).getSerializedStateByValue ===
'function' &&
typeof (unknownApi as HasLibraryTransforms<StateT>).checkForDuplicateTitle === 'function'
);
};
/**
* @deprecated use HasLibraryTransforms instead
*/
export type HasLegacyLibraryTransforms = Pick<
HasLibraryTransforms,
'canLinkToLibrary' | 'canUnlinkFromLibrary'
> & {
linkToLibrary: () => Promise<void>;
unlinkFromLibrary: () => Promise<void>;
};
/**
* @deprecated use apiHasLibraryTransforms instead
*/
export const apiHasLegacyLibraryTransforms = (
unknownApi: null | unknown
): unknownApi is HasLegacyLibraryTransforms => {
return Boolean(
unknownApi &&
typeof (unknownApi as HasLegacyLibraryTransforms).canLinkToLibrary === 'function' &&
typeof (unknownApi as HasLegacyLibraryTransforms).canUnlinkFromLibrary === 'function' &&
typeof (unknownApi as HasLegacyLibraryTransforms).linkToLibrary === 'function' &&
typeof (unknownApi as HasLegacyLibraryTransforms).unlinkFromLibrary === 'function'
);
};

View file

@ -30,6 +30,9 @@ export const apiHasSerializableState = (api: unknown | null): api is HasSerializ
return Boolean((api as HasSerializableState)?.serializeState);
};
/**
* @deprecated use HasSerializableState instead
*/
export interface HasSnapshottableState<RuntimeState extends object = object> {
/**
* Serializes all runtime state exactly as it appears. This can be used
@ -38,6 +41,9 @@ export interface HasSnapshottableState<RuntimeState extends object = object> {
snapshotRuntimeState: () => RuntimeState;
}
/**
* @deprecated use apiHasSerializableState instead
*/
export const apiHasSnapshottableState = (api: unknown | null): api is HasSnapshottableState => {
return Boolean((api as HasSnapshottableState)?.snapshotRuntimeState);
};

View file

@ -10,6 +10,7 @@
"@kbn/es-query",
"@kbn/data-views-plugin",
"@kbn/expressions-plugin",
"@kbn/core-execution-context-common"
"@kbn/core-execution-context-common",
"@kbn/content-management-utils"
]
}

View file

@ -217,7 +217,7 @@ describe('getLinksEmbeddableFactory', () => {
},
references: [],
});
expect(api.libraryId$.value).toBe('123');
expect(await api.canUnlinkFromLibrary()).toBe(true);
expect(api.defaultPanelTitle!.value).toBe('links 001');
expect(api.defaultPanelDescription!.value).toBe('some links');
});
@ -236,8 +236,7 @@ describe('getLinksEmbeddableFactory', () => {
await waitFor(async () => {
const api = onApiAvailable.mock.calls[0][0];
api.unlinkFromLibrary();
expect(await api.serializeState()).toEqual({
expect(await api.getSerializedStateByValue()).toEqual({
rawState: {
title: 'my links',
description: 'just a few links',
@ -251,7 +250,6 @@ describe('getLinksEmbeddableFactory', () => {
},
references,
});
expect(api.libraryId$.value).toBeUndefined();
});
});
});
@ -329,7 +327,7 @@ describe('getLinksEmbeddableFactory', () => {
references,
});
expect(api.libraryId$.value).toBeUndefined();
expect(await api.canLinkToLibrary()).toBe(true);
});
});
test('save to library', async () => {
@ -355,8 +353,7 @@ describe('getLinksEmbeddableFactory', () => {
options: { references },
});
expect(newId).toBe('333');
expect(api.libraryId$.value).toBe('333');
expect(await api.serializeState()).toEqual({
expect(await api.getSerializedStateByReference(newId)).toEqual({
rawState: {
savedObjectId: '333',
title: 'my links',

View file

@ -10,17 +10,16 @@
import React, { createContext, useMemo } from 'react';
import { cloneDeep } from 'lodash';
import { BehaviorSubject } from 'rxjs';
import fastIsEqual from 'fast-deep-equal';
import { EuiListGroup, EuiPanel } from '@elastic/eui';
import { PanelIncompatibleError, ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import {
SerializedTitles,
initializeTitles,
SerializedPanelState,
useBatchedOptionalPublishingSubjects,
} from '@kbn/presentation-publishing';
import { SerializedPanelState } from '@kbn/presentation-containers';
import {
CONTENT_ID,
DASHBOARD_LINK_TYPE,
@ -105,8 +104,27 @@ export const getLinksEmbeddableFactory = () => {
state.defaultPanelDescription
);
const savedObjectId$ = new BehaviorSubject(state.savedObjectId);
const isByReference = Boolean(state.savedObjectId);
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
const serializeLinksState = (byReference: boolean, newId?: string) => {
if (byReference) {
const linksByReferenceState: LinksByReferenceSerializedState = {
savedObjectId: newId ?? state.savedObjectId!,
...serializeTitles(),
};
return { rawState: linksByReferenceState, references: [] };
}
const runtimeState = api.snapshotRuntimeState();
const { attributes, references } = serializeLinksAttributes(runtimeState);
const linksByValueState: LinksByValueSerializedState = {
attributes,
...serializeTitles(),
};
return { rawState: linksByValueState, references };
};
const api = buildApi(
{
...titlesApi,
@ -114,29 +132,8 @@ export const getLinksEmbeddableFactory = () => {
defaultPanelTitle,
defaultPanelDescription,
isEditingEnabled: () => Boolean(error$.value === undefined),
libraryId$: savedObjectId$,
getTypeDisplayName: () => DISPLAY_NAME,
getByValueRuntimeSnapshot: () => {
const snapshot = api.snapshotRuntimeState();
delete snapshot.savedObjectId;
return snapshot;
},
serializeState: (): SerializedPanelState<LinksSerializedState> => {
if (savedObjectId$.value !== undefined) {
const linksByReferenceState: LinksByReferenceSerializedState = {
savedObjectId: savedObjectId$.value,
...serializeTitles(),
};
return { rawState: linksByReferenceState, references: [] };
}
const runtimeState = api.snapshotRuntimeState();
const { attributes, references } = serializeLinksAttributes(runtimeState);
const linksByValueState: LinksByValueSerializedState = {
attributes,
...serializeTitles(),
};
return { rawState: linksByValueState, references };
},
serializeState: () => serializeLinksState(isByReference),
saveToLibrary: async (newTitle: string) => {
defaultPanelTitle.next(newTitle);
const runtimeState = api.snapshotRuntimeState();
@ -150,9 +147,17 @@ export const getLinksEmbeddableFactory = () => {
},
options: { references },
});
savedObjectId$.next(id);
return id;
},
getSerializedStateByValue: () =>
serializeLinksState(false) as SerializedPanelState<LinksByValueSerializedState>,
getSerializedStateByReference: (newId: string) =>
serializeLinksState(
true,
newId
) as SerializedPanelState<LinksByReferenceSerializedState>,
canLinkToLibrary: async () => !isByReference,
canUnlinkFromLibrary: async () => isByReference,
checkForDuplicateTitle: async (
newTitle: string,
isTitleDuplicateConfirmed: boolean,
@ -166,36 +171,41 @@ export const getLinksEmbeddableFactory = () => {
onTitleDuplicate,
});
},
unlinkFromLibrary: () => {
savedObjectId$.next(undefined);
},
onEdit: async () => {
const { openEditorFlyout } = await import('../editor/open_editor_flyout');
const newState = await openEditorFlyout({
initialState: api.snapshotRuntimeState(),
parentDashboard: parentApi,
});
if (!newState) return;
if (newState) {
links$.next(newState.links);
layout$.next(newState.layout);
defaultPanelTitle.next(newState.defaultPanelTitle);
defaultPanelDescription.next(newState.defaultPanelDescription);
savedObjectId$.next(newState.savedObjectId);
// if the by reference state has changed during this edit, reinitialize the panel.
const nextIsByReference = Boolean(newState?.savedObjectId);
if (nextIsByReference !== isByReference) {
const serializedState = serializeLinksState(
nextIsByReference,
newState?.savedObjectId
);
(serializedState.rawState as SerializedTitles).title = newState.title;
api.parentApi?.replacePanel<LinksSerializedState>(api.uuid, {
serializedState,
panelType: api.type,
});
return;
}
links$.next(newState.links);
layout$.next(newState.layout);
defaultPanelTitle.next(newState.defaultPanelTitle);
defaultPanelDescription.next(newState.defaultPanelDescription);
},
},
{
...titleComparators,
links: [
links$,
(nextLinks?: ResolvedLink[]) => links$.next(nextLinks ?? []),
(a, b) => Boolean(savedObjectId$.value) || fastIsEqual(a, b), // Editing attributes in a by-reference panel should not trigger unsaved changes.
],
links: [links$, (nextLinks?: ResolvedLink[]) => links$.next(nextLinks ?? [])],
layout: [
layout$,
(nextLayout?: LinksLayoutType) => layout$.next(nextLayout ?? LINKS_VERTICAL_LAYOUT),
(a, b) => Boolean(savedObjectId$.value) || a === b,
],
error: [error$, (nextError?: Error) => error$.next(nextError)],
defaultPanelDescription: [

View file

@ -9,7 +9,7 @@
import {
HasEditCapabilities,
HasInPlaceLibraryTransforms,
HasLibraryTransforms,
HasType,
PublishesPanelDescription,
PublishesPanelTitle,
@ -40,7 +40,7 @@ export type LinksParentApi = PresentationContainer &
export type LinksApi = HasType<typeof CONTENT_ID> &
DefaultEmbeddableApi<LinksSerializedState, LinksRuntimeState> &
HasEditCapabilities &
HasInPlaceLibraryTransforms<LinksRuntimeState>;
HasLibraryTransforms<LinksByReferenceSerializedState, LinksByValueSerializedState>;
export interface LinksByReferenceSerializedState {
savedObjectId: string;

View file

@ -17,8 +17,11 @@ import type {
PanelPackage,
PresentationContainer,
} from '@kbn/presentation-containers';
import { apiHasSnapshottableState } from '@kbn/presentation-containers/interfaces/serialized_state';
import type { PublishingSubject, StateComparators } from '@kbn/presentation-publishing';
import {
type PublishingSubject,
type StateComparators,
apiHasSnapshottableState,
} from '@kbn/presentation-publishing';
import { BehaviorSubject, first, merge } from 'rxjs';
import type {
ControlPanelState,
@ -99,7 +102,7 @@ export function initControlsManager(
}
async function addNewPanel(
{ panelType, initialState }: PanelPackage<DefaultControlState>,
{ panelType, initialState }: PanelPackage<{}, DefaultControlState>,
index: number
) {
if ((initialState as DefaultDataControlState)?.dataViewId) {

View file

@ -9,7 +9,7 @@
import { omit } from 'lodash';
import { SerializedPanelState } from '@kbn/presentation-containers';
import { SerializedPanelState } from '@kbn/presentation-publishing';
import type { ControlGroupRuntimeState, ControlGroupSerializedState } from '../../../common';
import { parseReferenceName } from '../../controls/data_controls/reference_name_utils';

View file

@ -16,8 +16,7 @@ import {
DataViewField,
} from '@kbn/data-views-plugin/common';
import { Filter } from '@kbn/es-query';
import { SerializedPanelState } from '@kbn/presentation-containers';
import { StateComparators } from '@kbn/presentation-publishing';
import { StateComparators, SerializedPanelState } from '@kbn/presentation-publishing';
import { i18n } from '@kbn/i18n';
import type { DefaultControlState, DefaultDataControlState } from '../../../common';

View file

@ -12,7 +12,7 @@ import { of } from 'rxjs';
import { estypes } from '@elastic/elasticsearch';
import { DataViewField } from '@kbn/data-views-plugin/common';
import { SerializedPanelState } from '@kbn/presentation-containers';
import { SerializedPanelState } from '@kbn/presentation-publishing';
import { fireEvent, render, waitFor } from '@testing-library/react';
import { dataService, dataViewsService } from '../../../services/kibana_services';

View file

@ -8,9 +8,7 @@
*/
import { BehaviorSubject } from 'rxjs';
import { SerializedPanelState } from '@kbn/presentation-containers';
import { StateComparators } from '@kbn/presentation-publishing';
import { StateComparators, SerializedPanelState } from '@kbn/presentation-publishing';
import type { ControlWidth, DefaultControlState } from '../../common';
import type { ControlApiInitialization, ControlStateManager, DefaultControlApi } from './types';

View file

@ -9,12 +9,12 @@
import { BehaviorSubject } from 'rxjs';
import { HasSerializableState } from '@kbn/presentation-containers';
import { PanelCompatibleComponent } from '@kbn/presentation-panel-plugin/public/panel_component/types';
import {
HasParentApi,
HasType,
HasUniqueId,
HasSerializableState,
PublishesBlockingError,
PublishesDataLoading,
PublishesDisabledActionIds,

View file

@ -11,8 +11,6 @@ export { ClonePanelAction } from './clone_panel_action';
export { ExpandPanelAction } from './expand_panel_action';
export { FiltersNotificationAction } from './filters_notification_action';
export { ExportCSVAction } from './export_csv_action';
export { AddToLibraryAction } from './add_to_library_action';
export { LegacyAddToLibraryAction } from './legacy_add_to_library_action';
export { AddToLibraryAction } from './library_add_action';
export { UnlinkFromLibraryAction } from './library_unlink_action';
export { CopyToDashboardAction } from './copy_to_dashboard_action';
export { UnlinkFromLibraryAction } from './unlink_from_library_action';
export { LegacyUnlinkFromLibraryAction } from './legacy_unlink_from_library_action';

View file

@ -21,6 +21,11 @@ describe('Clone panel action', () => {
embeddable: {
uuid: 'superId',
viewMode: new BehaviorSubject<ViewMode>('edit'),
serializeState: () => {
return {
rawState: {},
};
},
parentApi: {
duplicatePanel: jest.fn(),
},

View file

@ -17,6 +17,8 @@ import {
getInheritedViewMode,
HasParentApi,
PublishesBlockingError,
apiHasSerializableState,
HasSerializableState,
HasUniqueId,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
@ -24,6 +26,7 @@ import { dashboardClonePanelActionStrings } from './_dashboard_actions_strings';
import { ACTION_CLONE_PANEL, DASHBOARD_ACTION_GROUP } from './constants';
export type ClonePanelActionApi = CanAccessViewMode &
HasSerializableState &
HasUniqueId &
HasParentApi<CanDuplicatePanels> &
Partial<PublishesBlockingError>;
@ -31,6 +34,7 @@ export type ClonePanelActionApi = CanAccessViewMode &
const isApiCompatible = (api: unknown | null): api is ClonePanelActionApi =>
Boolean(
apiHasUniqueId(api) &&
apiHasSerializableState(api) &&
apiCanAccessViewMode(api) &&
apiHasParentApi(api) &&
apiCanDuplicatePanels(api.parentApi)

View file

@ -14,7 +14,5 @@ export const ACTION_CLONE_PANEL = 'clonePanel';
export const ACTION_COPY_TO_DASHBOARD = 'copyToDashboard';
export const ACTION_EXPAND_PANEL = 'togglePanel';
export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV';
export const ACTION_LEGACY_ADD_TO_LIBRARY = 'legacySaveToLibrary';
export const ACTION_LEGACY_UNLINK_FROM_LIBRARY = 'legacyUnlinkFromLibrary';
export const ACTION_UNLINK_FROM_LIBRARY = 'unlinkFromLibrary';
export const BADGE_FILTERS_NOTIFICATION = 'ACTION_FILTERS_NOTIFICATION';

View file

@ -19,7 +19,7 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { EmbeddablePackageState, PanelNotFoundError } from '@kbn/embeddable-plugin/public';
import { apiHasSnapshottableState } from '@kbn/presentation-containers/interfaces/serialized_state';
import { apiHasSnapshottableState } from '@kbn/presentation-publishing';
import { LazyDashboardPicker, withSuspense } from '@kbn/presentation-util-plugin/public';
import { omit } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';

View file

@ -1,79 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import {
LegacyAddToLibraryAction,
LegacyAddPanelToLibraryActionApi,
} from './legacy_add_to_library_action';
import { coreServices } from '../services/kibana_services';
describe('Add to library action', () => {
let action: LegacyAddToLibraryAction;
let context: { embeddable: LegacyAddPanelToLibraryActionApi };
beforeEach(() => {
action = new LegacyAddToLibraryAction();
context = {
embeddable: {
linkToLibrary: jest.fn(),
canLinkToLibrary: jest.fn().mockResolvedValue(true),
unlinkFromLibrary: jest.fn(),
canUnlinkFromLibrary: jest.fn().mockResolvedValue(true),
viewMode: new BehaviorSubject<ViewMode>('edit'),
panelTitle: new BehaviorSubject<string | undefined>('A very compatible API'),
},
};
});
it('is compatible when api meets all conditions', async () => {
expect(await action.isCompatible(context)).toBe(true);
});
it('is incompatible when context lacks necessary functions', async () => {
const emptyContext = {
embeddable: {},
};
expect(await action.isCompatible(emptyContext)).toBe(false);
});
it('is incompatible when view mode is view', async () => {
(context.embeddable as PublishesViewMode).viewMode = new BehaviorSubject<ViewMode>('view');
expect(await action.isCompatible(context)).toBe(false);
});
it('is incompatible when canLinkToLibrary returns false', async () => {
context.embeddable.canLinkToLibrary = jest.fn().mockResolvedValue(false);
expect(await action.isCompatible(context)).toBe(false);
});
it('calls the linkToLibrary method on execute', async () => {
action.execute(context);
expect(context.embeddable.linkToLibrary).toHaveBeenCalled();
});
it('shows a toast with a title from the API when successful', async () => {
await action.execute(context);
expect(coreServices.notifications.toasts.addSuccess).toHaveBeenCalledWith({
'data-test-subj': 'addPanelToLibrarySuccess',
title: "Panel 'A very compatible API' was added to the library",
});
});
it('shows a danger toast when the link operation is unsuccessful', async () => {
context.embeddable.linkToLibrary = jest.fn().mockRejectedValue(new Error('Oh dang'));
await action.execute(context);
expect(coreServices.notifications.toasts.addDanger).toHaveBeenCalledWith({
'data-test-subj': 'addPanelToLibraryError',
title: 'An error was encountered adding panel A very compatible API to the library',
});
});
});

View file

@ -1,72 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import {
apiCanAccessViewMode,
apiHasLegacyLibraryTransforms,
EmbeddableApiContext,
getPanelTitle,
PublishesPanelTitle,
CanAccessViewMode,
getInheritedViewMode,
HasLegacyLibraryTransforms,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { dashboardAddToLibraryActionStrings } from './_dashboard_actions_strings';
import { coreServices } from '../services/kibana_services';
import { ACTION_LEGACY_ADD_TO_LIBRARY, DASHBOARD_ACTION_GROUP } from './constants';
export type LegacyAddPanelToLibraryActionApi = CanAccessViewMode &
HasLegacyLibraryTransforms &
Partial<PublishesPanelTitle>;
const isApiCompatible = (api: unknown | null): api is LegacyAddPanelToLibraryActionApi =>
Boolean(apiCanAccessViewMode(api) && apiHasLegacyLibraryTransforms(api));
export class LegacyAddToLibraryAction implements Action<EmbeddableApiContext> {
public readonly type = ACTION_LEGACY_ADD_TO_LIBRARY;
public readonly id = ACTION_LEGACY_ADD_TO_LIBRARY;
public order = 15;
public grouping = [DASHBOARD_ACTION_GROUP];
public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
return dashboardAddToLibraryActionStrings.getDisplayName();
}
public getIconType({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
return 'folderCheck';
}
public async isCompatible({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) return false;
return getInheritedViewMode(embeddable) === 'edit' && (await embeddable.canLinkToLibrary());
}
public async execute({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
const panelTitle = getPanelTitle(embeddable);
try {
await embeddable.linkToLibrary();
coreServices.notifications.toasts.addSuccess({
title: dashboardAddToLibraryActionStrings.getSuccessMessage(
panelTitle ? `'${panelTitle}'` : ''
),
'data-test-subj': 'addPanelToLibrarySuccess',
});
} catch (e) {
coreServices.notifications.toasts.addDanger({
title: dashboardAddToLibraryActionStrings.getErrorMessage(panelTitle),
'data-test-subj': 'addPanelToLibraryError',
});
}
}
}

View file

@ -1,78 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { coreServices } from '../services/kibana_services';
import {
LegacyUnlinkFromLibraryAction,
LegacyUnlinkPanelFromLibraryActionApi,
} from './legacy_unlink_from_library_action';
describe('Unlink from library action', () => {
let action: LegacyUnlinkFromLibraryAction;
let context: { embeddable: LegacyUnlinkPanelFromLibraryActionApi };
beforeEach(() => {
action = new LegacyUnlinkFromLibraryAction();
context = {
embeddable: {
unlinkFromLibrary: jest.fn(),
canUnlinkFromLibrary: jest.fn().mockResolvedValue(true),
linkToLibrary: jest.fn(),
canLinkToLibrary: jest.fn().mockResolvedValue(true),
viewMode: new BehaviorSubject<ViewMode>('edit'),
panelTitle: new BehaviorSubject<string | undefined>('A very compatible API'),
},
};
});
it('is compatible when api meets all conditions', async () => {
expect(await action.isCompatible(context)).toBe(true);
});
it('is incompatible when context lacks necessary functions', async () => {
const emptyContext = {
embeddable: {},
};
expect(await action.isCompatible(emptyContext)).toBe(false);
});
it('is incompatible when view mode is view', async () => {
(context.embeddable as PublishesViewMode).viewMode = new BehaviorSubject<ViewMode>('view');
expect(await action.isCompatible(context)).toBe(false);
});
it('is incompatible when canUnlinkFromLibrary returns false', async () => {
context.embeddable.canUnlinkFromLibrary = jest.fn().mockResolvedValue(false);
expect(await action.isCompatible(context)).toBe(false);
});
it('calls the unlinkFromLibrary method on execute', async () => {
action.execute(context);
expect(context.embeddable.unlinkFromLibrary).toHaveBeenCalled();
});
it('shows a toast with a title from the API when successful', async () => {
await action.execute(context);
expect(coreServices.notifications.toasts.addSuccess).toHaveBeenCalledWith({
'data-test-subj': 'unlinkPanelSuccess',
title: "Panel 'A very compatible API' is no longer connected to the library.",
});
});
it('shows a danger toast when the link operation is unsuccessful', async () => {
context.embeddable.unlinkFromLibrary = jest.fn().mockRejectedValue(new Error('Oh dang'));
await action.execute(context);
expect(coreServices.notifications.toasts.addDanger).toHaveBeenCalledWith({
'data-test-subj': 'unlinkPanelFailure',
title: "An error occured while unlinking 'A very compatible API' from the library.",
});
});
});

View file

@ -1,72 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import {
apiCanAccessViewMode,
apiHasLegacyLibraryTransforms,
CanAccessViewMode,
EmbeddableApiContext,
getInheritedViewMode,
getPanelTitle,
PublishesPanelTitle,
HasLegacyLibraryTransforms,
} from '@kbn/presentation-publishing';
import { dashboardUnlinkFromLibraryActionStrings } from './_dashboard_actions_strings';
import { coreServices } from '../services/kibana_services';
import { ACTION_LEGACY_UNLINK_FROM_LIBRARY, DASHBOARD_ACTION_GROUP } from './constants';
export type LegacyUnlinkPanelFromLibraryActionApi = CanAccessViewMode &
HasLegacyLibraryTransforms &
Partial<PublishesPanelTitle>;
export const legacyUnlinkActionIsCompatible = (
api: unknown | null
): api is LegacyUnlinkPanelFromLibraryActionApi =>
Boolean(apiCanAccessViewMode(api) && apiHasLegacyLibraryTransforms(api));
export class LegacyUnlinkFromLibraryAction implements Action<EmbeddableApiContext> {
public readonly type = ACTION_LEGACY_UNLINK_FROM_LIBRARY;
public readonly id = ACTION_LEGACY_UNLINK_FROM_LIBRARY;
public order = 15;
public grouping = [DASHBOARD_ACTION_GROUP];
public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!legacyUnlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
return dashboardUnlinkFromLibraryActionStrings.getDisplayName();
}
public getIconType({ embeddable }: EmbeddableApiContext) {
if (!legacyUnlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
return 'folderExclamation';
}
public async isCompatible({ embeddable }: EmbeddableApiContext) {
if (!legacyUnlinkActionIsCompatible(embeddable)) return false;
return getInheritedViewMode(embeddable) === 'edit' && (await embeddable.canUnlinkFromLibrary());
}
public async execute({ embeddable }: EmbeddableApiContext) {
if (!legacyUnlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
const title = getPanelTitle(embeddable);
try {
await embeddable.unlinkFromLibrary();
coreServices.notifications.toasts.addSuccess({
title: dashboardUnlinkFromLibraryActionStrings.getSuccessMessage(title ? `'${title}'` : ''),
'data-test-subj': 'unlinkPanelSuccess',
});
} catch (e) {
coreServices.notifications.toasts.addDanger({
title: dashboardUnlinkFromLibraryActionStrings.getFailureMessage(title ? `'${title}'` : ''),
'data-test-subj': 'unlinkPanelFailure',
});
}
}
}

View file

@ -9,11 +9,10 @@
import React from 'react';
import { PresentationContainer } from '@kbn/presentation-containers';
import { PanelPackage, PresentationContainer } from '@kbn/presentation-containers';
import {
CanAccessViewMode,
EmbeddableApiContext,
HasInPlaceLibraryTransforms,
HasLibraryTransforms,
HasParentApi,
HasType,
@ -21,7 +20,6 @@ import {
HasUniqueId,
PublishesPanelTitle,
apiCanAccessViewMode,
apiHasInPlaceLibraryTransforms,
apiHasLibraryTransforms,
apiHasParentApi,
apiHasType,
@ -44,14 +42,14 @@ import { ACTION_ADD_TO_LIBRARY, DASHBOARD_ACTION_GROUP } from './constants';
export type AddPanelToLibraryActionApi = CanAccessViewMode &
HasType &
HasUniqueId &
(HasLibraryTransforms | HasInPlaceLibraryTransforms) &
HasLibraryTransforms &
HasParentApi<Pick<PresentationContainer, 'replacePanel'>> &
Partial<PublishesPanelTitle & HasTypeDisplayName>;
const isApiCompatible = (api: unknown | null): api is AddPanelToLibraryActionApi =>
Boolean(
apiCanAccessViewMode(api) &&
(apiHasLibraryTransforms(api) || apiHasInPlaceLibraryTransforms(api)) &&
apiHasLibraryTransforms(api) &&
apiHasType(api) &&
apiHasUniqueId(api) &&
apiHasParentApi(api) &&
@ -76,27 +74,37 @@ export class AddToLibraryAction implements Action<EmbeddableApiContext> {
public async isCompatible({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) return false;
return getInheritedViewMode(embeddable) === 'edit' && (await this.canLinkToLibrary(embeddable));
return getInheritedViewMode(embeddable) === 'edit' && (await embeddable.canLinkToLibrary());
}
public async execute({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
const title = getPanelTitle(embeddable);
const lastTitle = getPanelTitle(embeddable);
try {
const byRefState = await new Promise<object | undefined>((resolve, reject) => {
const onSave = async (props: OnSaveProps): Promise<SaveResult> => {
const { byRefPackage, libraryTitle } = await new Promise<{
byRefPackage: PanelPackage;
libraryTitle: string;
}>((resolve, reject) => {
const onSave = async ({
newTitle,
isTitleDuplicateConfirmed,
onTitleDuplicate,
}: OnSaveProps): Promise<SaveResult> => {
await embeddable.checkForDuplicateTitle(
props.newTitle,
props.isTitleDuplicateConfirmed,
props.onTitleDuplicate
newTitle,
isTitleDuplicateConfirmed,
onTitleDuplicate
);
try {
const libraryId = await embeddable.saveToLibrary(props.newTitle);
if (apiHasLibraryTransforms(embeddable)) {
resolve({ ...embeddable.getByReferenceState(libraryId), title: props.newTitle });
}
resolve(undefined);
const libraryId = await embeddable.saveToLibrary(newTitle);
const { rawState, references } = embeddable.getSerializedStateByReference(libraryId);
resolve({
byRefPackage: {
serializedState: { rawState: { ...rawState, title: newTitle }, references },
panelType: embeddable.type,
},
libraryTitle: newTitle,
});
return { id: libraryId };
} catch (error) {
reject(error);
@ -107,7 +115,7 @@ export class AddToLibraryAction implements Action<EmbeddableApiContext> {
<SavedObjectSaveModal
onSave={onSave}
onClose={() => {}}
title={title ?? ''}
title={lastTitle ?? ''}
showCopyOnSave={false}
objectType={
typeof embeddable.getTypeDisplayName === 'function'
@ -118,35 +126,17 @@ export class AddToLibraryAction implements Action<EmbeddableApiContext> {
/>
);
});
/**
* If byRefState is defined, this embeddable type must be re-initialized with the
* newly provided state.
*/
if (byRefState) {
await embeddable.parentApi.replacePanel(embeddable.uuid, {
panelType: embeddable.type,
initialState: byRefState,
});
}
await embeddable.parentApi.replacePanel(embeddable.uuid, byRefPackage);
coreServices.notifications.toasts.addSuccess({
title: dashboardAddToLibraryActionStrings.getSuccessMessage(title ? `'${title}'` : ''),
title: dashboardAddToLibraryActionStrings.getSuccessMessage(`'${libraryTitle}'`),
'data-test-subj': 'addPanelToLibrarySuccess',
});
} catch (e) {
coreServices.notifications.toasts.addDanger({
title: dashboardAddToLibraryActionStrings.getErrorMessage(title),
title: dashboardAddToLibraryActionStrings.getErrorMessage(lastTitle),
'data-test-subj': 'addPanelToLibraryError',
});
}
}
private async canLinkToLibrary(api: AddPanelToLibraryActionApi) {
if (apiHasLibraryTransforms(api)) {
return api.canLinkToLibrary?.();
} else if (apiHasInPlaceLibraryTransforms(api)) {
const canLink = api.canLinkToLibrary ? await api.canLinkToLibrary() : true;
return api.libraryId$.value === undefined && canLink;
}
throw new IncompatibleActionError();
}
}

View file

@ -11,14 +11,12 @@ import { PresentationContainer } from '@kbn/presentation-containers';
import {
CanAccessViewMode,
EmbeddableApiContext,
HasInPlaceLibraryTransforms,
HasLibraryTransforms,
HasParentApi,
HasType,
HasUniqueId,
PublishesPanelTitle,
apiCanAccessViewMode,
apiHasInPlaceLibraryTransforms,
apiHasLibraryTransforms,
apiHasParentApi,
apiHasType,
@ -33,7 +31,7 @@ import { dashboardUnlinkFromLibraryActionStrings } from './_dashboard_actions_st
import { ACTION_UNLINK_FROM_LIBRARY, DASHBOARD_ACTION_GROUP } from './constants';
export type UnlinkPanelFromLibraryActionApi = CanAccessViewMode &
(HasLibraryTransforms | HasInPlaceLibraryTransforms) &
HasLibraryTransforms &
HasType &
HasUniqueId &
HasParentApi<Pick<PresentationContainer, 'replacePanel'>> &
@ -42,7 +40,7 @@ export type UnlinkPanelFromLibraryActionApi = CanAccessViewMode &
export const isApiCompatible = (api: unknown | null): api is UnlinkPanelFromLibraryActionApi =>
Boolean(
apiCanAccessViewMode(api) &&
(apiHasLibraryTransforms(api) || apiHasInPlaceLibraryTransforms(api)) &&
apiHasLibraryTransforms(api) &&
apiHasUniqueId(api) &&
apiHasType(api) &&
apiHasParentApi(api) &&
@ -65,40 +63,23 @@ export class UnlinkFromLibraryAction implements Action<EmbeddableApiContext> {
return 'folderExclamation';
}
public async canUnlinkFromLibrary(api: UnlinkPanelFromLibraryActionApi) {
if (apiHasLibraryTransforms(api)) {
return api.canUnlinkFromLibrary();
} else if (apiHasInPlaceLibraryTransforms(api)) {
const canUnLink = api.canUnlinkFromLibrary ? await api.canUnlinkFromLibrary() : true;
return canUnLink && Boolean(api.libraryId$.value);
}
throw new IncompatibleActionError();
}
public async isCompatible({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) {
// either a an `unlinkFromLibrary` method or a `getByValueState` method is required
return false;
}
return (
getInheritedViewMode(embeddable) === 'edit' && (await this.canUnlinkFromLibrary(embeddable))
);
return getInheritedViewMode(embeddable) === 'edit' && (await embeddable.canUnlinkFromLibrary());
}
public async execute({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
const title = getPanelTitle(embeddable);
try {
if (apiHasLibraryTransforms(embeddable)) {
await embeddable.parentApi.replacePanel(embeddable.uuid, {
panelType: embeddable.type,
initialState: { ...embeddable.getByValueState(), title },
});
} else if (apiHasInPlaceLibraryTransforms(embeddable)) {
embeddable.unlinkFromLibrary();
} else {
throw new IncompatibleActionError();
}
const { references, rawState } = embeddable.getSerializedStateByValue();
await embeddable.parentApi.replacePanel(embeddable.uuid, {
panelType: embeddable.type,
serializedState: { rawState: { ...rawState, title }, references },
});
coreServices.notifications.toasts.addSuccess({
title: dashboardUnlinkFromLibraryActionStrings.getSuccessMessage(title ? `'${title}'` : ''),
'data-test-subj': 'unlinkPanelSuccess',

View file

@ -15,8 +15,6 @@ import {
ACTION_COPY_TO_DASHBOARD,
ACTION_EXPAND_PANEL,
ACTION_EXPORT_CSV,
ACTION_LEGACY_ADD_TO_LIBRARY,
ACTION_LEGACY_UNLINK_FROM_LIBRARY,
ACTION_UNLINK_FROM_LIBRARY,
BADGE_FILTERS_NOTIFICATION,
} from './constants';
@ -63,24 +61,12 @@ export const registerActions = async ({
});
uiActions.attachAction(CONTEXT_MENU_TRIGGER, ACTION_ADD_TO_LIBRARY);
uiActions.registerActionAsync(ACTION_LEGACY_ADD_TO_LIBRARY, async () => {
const { LegacyAddToLibraryAction } = await import('./actions_module');
return new LegacyAddToLibraryAction();
});
uiActions.attachAction(CONTEXT_MENU_TRIGGER, ACTION_LEGACY_ADD_TO_LIBRARY);
uiActions.registerActionAsync(ACTION_UNLINK_FROM_LIBRARY, async () => {
const { UnlinkFromLibraryAction } = await import('./actions_module');
return new UnlinkFromLibraryAction();
});
uiActions.attachAction(CONTEXT_MENU_TRIGGER, ACTION_UNLINK_FROM_LIBRARY);
uiActions.registerActionAsync(ACTION_LEGACY_UNLINK_FROM_LIBRARY, async () => {
const { LegacyUnlinkFromLibraryAction } = await import('./actions_module');
return new LegacyUnlinkFromLibraryAction();
});
uiActions.attachAction(CONTEXT_MENU_TRIGGER, ACTION_LEGACY_UNLINK_FROM_LIBRARY);
uiActions.registerActionAsync(ACTION_COPY_TO_DASHBOARD, async () => {
const { CopyToDashboardAction } = await import('./actions_module');
return new CopyToDashboardAction();

View file

@ -7,23 +7,33 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { BehaviorSubject, debounceTime, merge } from 'rxjs';
import { omit } from 'lodash';
import { v4 } from 'uuid';
import type { Reference } from '@kbn/content-management-utils';
import { ControlGroupApi, ControlGroupSerializedState } from '@kbn/controls-plugin/public';
import { EmbeddablePackageState } from '@kbn/embeddable-plugin/public';
import { StateComparators } from '@kbn/presentation-publishing';
import { omit } from 'lodash';
import { BehaviorSubject, debounceTime, merge } from 'rxjs';
import { v4 } from 'uuid';
import {
getReferencesForControls,
getReferencesForPanelId,
} from '../../common/dashboard_container/persistable_state/dashboard_container_references';
import { initializeTrackPanel } from './track_panel';
import { initializeTrackOverlay } from './track_overlay';
import { initializeUnsavedChangesManager } from './unsaved_changes_manager';
import { UnsavedPanelState } from '../dashboard_container/types';
import { DASHBOARD_APP_ID } from '../plugin_constants';
import { DEFAULT_DASHBOARD_INPUT } from './default_dashboard_input';
import { PANELS_CONTROL_GROUP_KEY } from '../services/dashboard_backup_service';
import { getDashboardContentManagementService } from '../services/dashboard_content_management_service';
import { LoadDashboardReturn } from '../services/dashboard_content_management_service/types';
import { initializeDataLoadingManager } from './data_loading_manager';
import { initializeDataViewsManager } from './data_views_manager';
import { DEFAULT_DASHBOARD_INPUT } from './default_dashboard_input';
import { getSerializedState } from './get_serialized_state';
import { openSaveModal } from './open_save_modal';
import { initializePanelsManager } from './panels_manager';
import { initializeSearchSessionManager } from './search_session_manager';
import { initializeSettingsManager } from './settings_manager';
import { initializeTrackContentfulRender } from './track_contentful_render';
import { initializeTrackOverlay } from './track_overlay';
import { initializeTrackPanel } from './track_panel';
import {
DASHBOARD_API_TYPE,
DashboardApi,
@ -31,18 +41,9 @@ import {
DashboardInternalApi,
DashboardState,
} from './types';
import { initializeDataViewsManager } from './data_views_manager';
import { initializeSettingsManager } from './settings_manager';
import { initializeUnifiedSearchManager } from './unified_search_manager';
import { initializeDataLoadingManager } from './data_loading_manager';
import { PANELS_CONTROL_GROUP_KEY } from '../services/dashboard_backup_service';
import { getDashboardContentManagementService } from '../services/dashboard_content_management_service';
import { openSaveModal } from './open_save_modal';
import { initializeSearchSessionManager } from './search_session_manager';
import { initializeUnsavedChangesManager } from './unsaved_changes_manager';
import { initializeViewModeManager } from './view_mode_manager';
import { UnsavedPanelState } from '../dashboard_container/types';
import { initializeTrackContentfulRender } from './track_contentful_render';
import { getSerializedState } from './get_serialized_state';
export function getDashboardApi({
creationOptions,
@ -62,26 +63,34 @@ export function getDashboardApi({
const controlGroupApi$ = new BehaviorSubject<ControlGroupApi | undefined>(undefined);
const fullScreenMode$ = new BehaviorSubject(creationOptions?.fullScreenMode ?? false);
const isManaged = savedObjectResult?.managed ?? false;
let references: Reference[] = savedObjectResult?.references ?? [];
const savedObjectId$ = new BehaviorSubject<string | undefined>(savedObjectId);
const viewModeManager = initializeViewModeManager(incomingEmbeddable, savedObjectResult);
const trackPanel = initializeTrackPanel(
async (id: string) => await panelsManager.api.untilEmbeddableLoaded(id)
);
function getPanelReferences(id: string) {
const panelReferences = getReferencesForPanelId(id, references);
const references$ = new BehaviorSubject<Reference[] | undefined>(initialState.references);
const getPanelReferences = (id: string) => {
const panelReferences = getReferencesForPanelId(id, references$.value ?? []);
// references from old installations may not be prefixed with panel id
// fall back to passing all references in these cases to preserve backwards compatability
return panelReferences.length > 0 ? panelReferences : references;
}
return panelReferences.length > 0 ? panelReferences : references$.value ?? [];
};
const pushPanelReferences = (refs: Reference[]) => {
references$.next([...(references$.value ?? []), ...refs]);
};
const referencesComparator: StateComparators<Pick<DashboardState, 'references'>> = {
references: [references$, (nextRefs) => references$.next(nextRefs)],
};
const panelsManager = initializePanelsManager(
incomingEmbeddable,
initialState.panels,
initialPanelsRuntimeState ?? {},
trackPanel,
getPanelReferences,
(refs: Reference[]) => references.push(...refs)
pushPanelReferences
);
const dataLoadingManager = initializeDataLoadingManager(panelsManager.api.children$);
const dataViewsManager = initializeDataViewsManager(
@ -108,6 +117,7 @@ export function getDashboardApi({
settingsManager,
viewModeManager,
unifiedSearchManager,
referencesComparator,
});
function getState() {
const { panels, references: panelReferences } = panelsManager.internalApi.getState();
@ -192,7 +202,7 @@ export function getDashboardApi({
});
savedObjectId$.next(saveResult.id);
references = saveResult.references ?? [];
references$.next(saveResult.references);
}
return saveResult;
@ -211,7 +221,7 @@ export function getDashboardApi({
});
unsavedChangesManager.internalApi.onSave(dashboardState);
references = saveResult.references ?? [];
references$.next(saveResult.references);
return;
},
@ -253,7 +263,7 @@ export function getDashboardApi({
labelPosition: 'oneLine',
showApplySelections: false,
} as ControlGroupSerializedState),
references: getReferencesForControls(references),
references: getReferencesForControls(references$.value ?? []),
};
},
getRuntimeStateForControlGroup: () => {

View file

@ -66,6 +66,9 @@ export async function loadDashboardApi({
...(savedObjectResult?.dashboardInput ?? {}),
...sessionStorageInput,
};
combinedSessionState.references = sessionStorageInput?.references?.length
? sessionStorageInput?.references
: savedObjectResult?.references;
// --------------------------------------------------------------------------------------
// Combine state with overrides.

View file

@ -13,7 +13,7 @@ import { v4 } from 'uuid';
import { asyncForEach } from '@kbn/std';
import type { Reference } from '@kbn/content-management-utils';
import { METRIC_TYPE } from '@kbn/analytics';
import { PanelPackage, apiHasSerializableState } from '@kbn/presentation-containers';
import { PanelPackage } from '@kbn/presentation-containers';
import {
DefaultEmbeddableApi,
EmbeddablePackageState,
@ -21,14 +21,12 @@ import {
} from '@kbn/embeddable-plugin/public';
import {
StateComparators,
apiHasInPlaceLibraryTransforms,
apiHasLibraryTransforms,
apiPublishesPanelTitle,
apiPublishesUnsavedChanges,
apiHasSerializableState,
getPanelTitle,
stateHasTitles,
} from '@kbn/presentation-publishing';
import { apiHasSnapshottableState } from '@kbn/presentation-containers/interfaces/serialized_state';
import { i18n } from '@kbn/i18n';
import { coreServices, usageCollectionService } from '../services/kibana_services';
import { DashboardPanelMap, DashboardPanelState, prefixReferencesFromPanel } from '../../common';
@ -171,72 +169,22 @@ export function initializePanelsManager(
return titles;
}
function duplicateReactEmbeddableInput(
childApi: unknown,
panelToClone: DashboardPanelState,
panelTitles: string[]
) {
const id = v4();
const lastTitle = apiPublishesPanelTitle(childApi) ? getPanelTitle(childApi) ?? '' : '';
const newTitle = getClonedPanelTitle(panelTitles, lastTitle);
/**
* For react embeddables that have library transforms, we need to ensure
* to clone them with serialized state and references.
*
* TODO: remove this section once all by reference capable react embeddables
* use in-place library transforms
*/
if (apiHasLibraryTransforms(childApi)) {
const byValueSerializedState = childApi.getByValueState();
if (panelToClone.references) {
pushReferences(prefixReferencesFromPanel(id, panelToClone.references));
}
return {
type: panelToClone.type,
explicitInput: {
...byValueSerializedState,
title: newTitle,
id,
},
};
}
const runtimeSnapshot = (() => {
if (apiHasInPlaceLibraryTransforms(childApi)) return childApi.getByValueRuntimeSnapshot();
return apiHasSnapshottableState(childApi) ? childApi.snapshotRuntimeState() : {};
})();
if (stateHasTitles(runtimeSnapshot)) runtimeSnapshot.title = newTitle;
setRuntimeStateForChild(id, runtimeSnapshot);
return {
type: panelToClone.type,
explicitInput: {
id,
},
};
}
return {
api: {
addNewPanel: async <ApiType extends unknown = unknown>(
panelPackage: PanelPackage,
displaySuccessMessage?: boolean
) => {
usageCollectionService?.reportUiCounter(
DASHBOARD_UI_METRIC_ID,
METRIC_TYPE.CLICK,
panelPackage.panelType
);
const { panelType: type, serializedState, initialState } = panelPackage;
usageCollectionService?.reportUiCounter(DASHBOARD_UI_METRIC_ID, METRIC_TYPE.CLICK, type);
const newId = v4();
const getCustomPlacementSettingFunc = getDashboardPanelPlacementSetting(
panelPackage.panelType
);
const getCustomPlacementSettingFunc = getDashboardPanelPlacementSetting(type);
const customPlacementSettings = getCustomPlacementSettingFunc
? await getCustomPlacementSettingFunc(panelPackage.initialState)
? await getCustomPlacementSettingFunc(initialState)
: undefined;
const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(
@ -247,19 +195,23 @@ export function initializePanelsManager(
width: customPlacementSettings?.width ?? DEFAULT_PANEL_WIDTH,
}
);
if (serializedState?.references && serializedState.references.length > 0) {
pushReferences(prefixReferencesFromPanel(newId, serializedState.references));
}
const newPanel: DashboardPanelState = {
type: panelPackage.panelType,
type,
gridData: {
...newPanelPlacement,
i: newId,
},
explicitInput: {
...serializedState?.rawState,
id: newId,
},
};
if (panelPackage.initialState) {
setRuntimeStateForChild(newId, panelPackage.initialState);
}
if (initialState) setRuntimeStateForChild(newId, initialState);
setPanels({ ...otherPanels, [newId]: newPanel });
if (displaySuccessMessage) {
coreServices.notifications.toasts.addSuccess({
@ -275,12 +227,27 @@ export function initializePanelsManager(
children$,
duplicatePanel: async (idToDuplicate: string) => {
const panelToClone = getDashboardPanelFromId(idToDuplicate);
const childApi = children$.value[idToDuplicate];
if (!apiHasSerializableState(childApi)) {
throw new Error('cannot duplicate a non-serializable panel');
}
const duplicatedPanelState = duplicateReactEmbeddableInput(
children$.value[idToDuplicate],
panelToClone,
await getPanelTitles()
);
const id = v4();
const allPanelTitles = await getPanelTitles();
const lastTitle = apiPublishesPanelTitle(childApi) ? getPanelTitle(childApi) ?? '' : '';
const newTitle = getClonedPanelTitle(allPanelTitles, lastTitle);
/**
* For embeddables that have library transforms, we need to ensure
* to clone them with by value serialized state.
*/
const serializedState = apiHasLibraryTransforms(childApi)
? childApi.getSerializedStateByValue()
: childApi.serializeState();
if (serializedState.references) {
pushReferences(prefixReferencesFromPanel(id, serializedState.references));
}
coreServices.notifications.toasts.addSuccess({
title: dashboardClonePanelActionStrings.getSuccessMessage(),
@ -295,16 +262,21 @@ export function initializePanelsManager(
});
const newPanel = {
...duplicatedPanelState,
type: panelToClone.type,
explicitInput: {
...serializedState.rawState,
title: newTitle,
id,
},
gridData: {
...newPanelPlacement,
i: duplicatedPanelState.explicitInput.id,
i: id,
},
};
setPanels({
...otherPanels,
[newPanel.explicitInput.id]: newPanel,
[id]: newPanel,
});
},
getDashboardPanelFromId,
@ -337,7 +309,7 @@ export function initializePanelsManager(
children$.next(children);
}
},
replacePanel: async (idToRemove: string, { panelType, initialState }: PanelPackage) => {
replacePanel: async (idToRemove: string, panelPackage: PanelPackage) => {
const panels = { ...panels$.value };
if (!panels[idToRemove]) {
throw new PanelNotFoundError();
@ -346,12 +318,20 @@ export function initializePanelsManager(
const id = v4();
const oldPanel = panels[idToRemove];
delete panels[idToRemove];
const { panelType: type, serializedState, initialState } = panelPackage;
if (serializedState?.references && serializedState.references.length > 0) {
pushReferences(prefixReferencesFromPanel(id, serializedState?.references));
}
if (initialState) setRuntimeStateForChild(id, initialState);
setPanels({
...panels,
[id]: {
...oldPanel,
explicitInput: { ...initialState, id },
type: panelType,
explicitInput: { ...serializedState?.rawState, id },
type,
},
});

View file

@ -7,6 +7,16 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { Reference } from '@kbn/content-management-utils';
import {
ControlGroupApi,
ControlGroupRuntimeState,
ControlGroupSerializedState,
} from '@kbn/controls-plugin/public';
import { RefreshInterval, SearchSessionInfoProvider } from '@kbn/data-plugin/public';
import type { DefaultEmbeddableApi, EmbeddablePackageState } from '@kbn/embeddable-plugin/public';
import { Filter, Query, TimeRange } from '@kbn/es-query';
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import {
CanExpandPanels,
HasRuntimeChildState,
@ -14,11 +24,11 @@ import {
HasSerializedChildState,
PresentationContainer,
PublishesSettings,
SerializedPanelState,
TrackContentfulRender,
TracksOverlays,
} from '@kbn/presentation-containers';
import {
SerializedPanelState,
EmbeddableAppContext,
HasAppContext,
HasExecutionContext,
@ -35,27 +45,17 @@ import {
PublishingSubject,
ViewMode,
} from '@kbn/presentation-publishing';
import {
ControlGroupApi,
ControlGroupRuntimeState,
ControlGroupSerializedState,
} from '@kbn/controls-plugin/public';
import { Filter, Query, TimeRange } from '@kbn/es-query';
import type { DefaultEmbeddableApi, EmbeddablePackageState } from '@kbn/embeddable-plugin/public';
import { Observable, Subject } from 'rxjs';
import { RefreshInterval, SearchSessionInfoProvider } from '@kbn/data-plugin/public';
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { PublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/publishes_reload';
import { PublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session';
import { LocatorPublic } from '@kbn/share-plugin/common';
import type { SavedObjectReference } from '@kbn/core-saved-objects-api-server';
import { Observable, Subject } from 'rxjs';
import { DashboardPanelMap, DashboardPanelState } from '../../common';
import type { DashboardAttributes, DashboardOptions } from '../../server/content_management';
import { DashboardLocatorParams } from '../dashboard_container/types';
import {
LoadDashboardReturn,
SaveDashboardReturn,
} from '../services/dashboard_content_management_service/types';
import { DashboardLocatorParams } from '../dashboard_container/types';
export const DASHBOARD_API_TYPE = 'dashboard';
@ -101,6 +101,12 @@ export interface DashboardState extends DashboardSettings {
viewMode: ViewMode;
panels: DashboardPanelMap;
/**
* Temporary. Currently Dashboards are in charge of providing references to all of their children.
* Eventually this will be removed in favour of the Dashboard injecting references serverside.
*/
references?: Reference[];
/**
* Serialized control group state.
* Contains state loaded from dashboard saved object
@ -145,7 +151,7 @@ export type DashboardApi = CanExpandPanels &
getSettings: () => DashboardSettings;
getSerializedState: () => {
attributes: DashboardAttributes;
references: SavedObjectReference[];
references: Reference[];
};
getDashboardPanelFromId: (id: string) => DashboardPanelState;
hasOverlays$: PublishingSubject<boolean>;

View file

@ -7,19 +7,23 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { BehaviorSubject, Subject, combineLatest, debounceTime, skipWhile, switchMap } from 'rxjs';
import { PublishesSavedObjectId, PublishingSubject } from '@kbn/presentation-publishing';
import { ControlGroupApi } from '@kbn/controls-plugin/public';
import { childrenUnsavedChanges$, initializeUnsavedChanges } from '@kbn/presentation-containers';
import {
PublishesSavedObjectId,
PublishingSubject,
StateComparators,
} from '@kbn/presentation-publishing';
import { omit } from 'lodash';
import { DashboardCreationOptions, DashboardState } from './types';
import { initializePanelsManager } from './panels_manager';
import { initializeSettingsManager } from './settings_manager';
import { initializeUnifiedSearchManager } from './unified_search_manager';
import { BehaviorSubject, Subject, combineLatest, debounceTime, skipWhile, switchMap } from 'rxjs';
import {
PANELS_CONTROL_GROUP_KEY,
getDashboardBackupService,
} from '../services/dashboard_backup_service';
import { initializePanelsManager } from './panels_manager';
import { initializeSettingsManager } from './settings_manager';
import { DashboardCreationOptions, DashboardState } from './types';
import { initializeUnifiedSearchManager } from './unified_search_manager';
import { initializeViewModeManager } from './view_mode_manager';
export function initializeUnsavedChangesManager({
@ -31,6 +35,7 @@ export function initializeUnsavedChangesManager({
settingsManager,
viewModeManager,
unifiedSearchManager,
referencesComparator,
}: {
creationOptions?: DashboardCreationOptions;
controlGroupApi$: PublishingSubject<ControlGroupApi | undefined>;
@ -40,6 +45,7 @@ export function initializeUnsavedChangesManager({
settingsManager: ReturnType<typeof initializeSettingsManager>;
viewModeManager: ReturnType<typeof initializeViewModeManager>;
unifiedSearchManager: ReturnType<typeof initializeUnifiedSearchManager>;
referencesComparator: StateComparators<Pick<DashboardState, 'references'>>;
}) {
const hasUnsavedChanges$ = new BehaviorSubject(false);
const lastSavedState$ = new BehaviorSubject<DashboardState>(lastSavedState);
@ -55,6 +61,7 @@ export function initializeUnsavedChangesManager({
...settingsManager.comparators,
...viewModeManager.comparators,
...unifiedSearchManager.comparators,
...referencesComparator,
}
);
@ -70,12 +77,14 @@ export function initializeUnsavedChangesManager({
])
.pipe(debounceTime(0))
.subscribe(([dashboardChanges, unsavedPanelState, controlGroupChanges]) => {
// viewMode needs to be stored in session state because
// its used to exclude 'view' dashboards on the listing page
// However, viewMode should not trigger unsaved changes notification
// otherwise, opening a dashboard in edit mode will always show unsaved changes
/**
* viewMode needs to be stored in session state because its used to exclude 'view' dashboards on the listing page
* However, viewMode differences should not trigger unsaved changes notification otherwise, opening a dashboard in
* edit mode will always show unsaved changes. Similarly, differences in references are derived from panels, so
* we don't consider them unsaved changes
*/
const hasDashboardChanges =
Object.keys(omit(dashboardChanges ?? {}, ['viewMode'])).length > 0;
Object.keys(omit(dashboardChanges ?? {}, ['viewMode', 'references'])).length > 0;
const hasUnsavedChanges =
hasDashboardChanges || unsavedPanelState !== undefined || controlGroupChanges !== undefined;
if (hasUnsavedChanges !== hasUnsavedChanges$.value) {

View file

@ -176,7 +176,7 @@ class DashboardBackupService implements DashboardBackupServiceType {
if (
dashboardStatesInSpace[dashboardId].viewMode === 'edit' &&
(Object.keys(dashboardStatesInSpace[dashboardId]).some(
(stateKey) => stateKey !== 'viewMode'
(stateKey) => stateKey !== 'viewMode' && stateKey !== 'references'
) ||
Object.keys(panelStatesInSpace?.[dashboardId]).length > 0)
)

View file

@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { omit } from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
@ -120,6 +119,16 @@ export const getSearchEmbeddableFactory = ({
stateManager: searchEmbeddable.stateManager,
});
const serialize = (savedObjectId?: string) =>
serializeState({
uuid,
initialState,
savedSearch: searchEmbeddable.api.savedSearch$.getValue(),
serializeTitles,
serializeTimeRange: timeRange.serialize,
savedObjectId,
});
const api: SearchEmbeddableApi = buildApi(
{
...titlesApi,
@ -137,15 +146,6 @@ export const getSearchEmbeddableFactory = ({
savedObjectId: savedObjectId$,
defaultPanelTitle: defaultPanelTitle$,
defaultPanelDescription: defaultPanelDescription$,
getByValueRuntimeSnapshot: () => {
const savedSearch = searchEmbeddable.api.savedSearch$.getValue();
return {
...serializeTitles(),
...timeRange.serialize(),
...omit(savedSearch, 'searchSource'),
serializedSearchSource: savedSearch.searchSource.getSerializedFields(),
};
},
hasTimeRange: () => {
const fetchContext = fetchContext$.getValue();
return fetchContext?.timeslice !== undefined || fetchContext?.timeRange !== undefined;
@ -160,14 +160,12 @@ export const getSearchEmbeddableFactory = ({
);
},
canUnlinkFromLibrary: async () => Boolean(savedObjectId$.getValue()),
libraryId$: savedObjectId$,
saveToLibrary: async (title: string) => {
const savedObjectId = await save({
...api.savedSearch$.getValue(),
title,
});
defaultPanelTitle$.next(title);
savedObjectId$.next(savedObjectId!);
return savedObjectId!;
},
checkForDuplicateTitle: (newTitle, isTitleDuplicateConfirmed, onTitleDuplicate) =>
@ -176,26 +174,9 @@ export const getSearchEmbeddableFactory = ({
isTitleDuplicateConfirmed,
onTitleDuplicate,
}),
unlinkFromLibrary: () => {
savedObjectId$.next(undefined);
if ((titlesApi.panelTitle.getValue() ?? '').length === 0) {
titlesApi.setPanelTitle(defaultPanelTitle$.getValue());
}
if ((titlesApi.panelDescription.getValue() ?? '').length === 0) {
titlesApi.setPanelDescription(defaultPanelDescription$.getValue());
}
defaultPanelTitle$.next(undefined);
defaultPanelDescription$.next(undefined);
},
serializeState: () =>
serializeState({
uuid,
initialState,
savedSearch: searchEmbeddable.api.savedSearch$.getValue(),
serializeTitles,
serializeTimeRange: timeRange.serialize,
savedObjectId: savedObjectId$.getValue(),
}),
getSerializedStateByValue: () => serialize(undefined),
getSerializedStateByReference: (newId: string) => serialize(newId),
serializeState: () => serialize(savedObjectId$.getValue()),
getInspectorAdapters: () => searchEmbeddable.stateManager.inspectorAdapters.getValue(),
},
{

View file

@ -13,7 +13,7 @@ import { HasInspectorAdapters } from '@kbn/inspector-plugin/public';
import {
EmbeddableApiContext,
HasEditCapabilities,
HasInPlaceLibraryTransforms,
HasLibraryTransforms,
PublishesBlockingError,
PublishesDataLoading,
PublishesSavedObjectId,
@ -104,7 +104,7 @@ export type SearchEmbeddableApi = DefaultEmbeddableApi<
PublishesSavedSearch &
PublishesWritableDataViews &
PublishesWritableUnifiedSearch &
HasInPlaceLibraryTransforms &
HasLibraryTransforms &
HasTimeRange &
HasInspectorAdapters &
Partial<HasEditCapabilities & PublishesSavedObjectId>;

View file

@ -10,7 +10,7 @@
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { SerializedPanelState } from '@kbn/presentation-containers';
import { SerializedPanelState } from '@kbn/presentation-publishing';
import { toSavedSearchAttributes } from '@kbn/saved-search-plugin/common';
import { SavedSearchUnwrapResult } from '@kbn/saved-search-plugin/public';
import { discoverServiceMock } from '../../__mocks__/services';

View file

@ -11,8 +11,11 @@ import { omit, pick } from 'lodash';
import deepEqual from 'react-fast-compare';
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
import { SerializedPanelState } from '@kbn/presentation-containers';
import { SerializedTimeRange, SerializedTitles } from '@kbn/presentation-publishing';
import {
SerializedTimeRange,
SerializedTitles,
SerializedPanelState,
} from '@kbn/presentation-publishing';
import {
SavedSearch,
SavedSearchAttributes,

View file

@ -11,12 +11,15 @@ import {
apiHasRuntimeChildState,
apiIsPresentationContainer,
HasSerializedChildState,
HasSnapshottableState,
initializeUnsavedChanges,
SerializedPanelState,
} from '@kbn/presentation-containers';
import { PresentationPanel, PresentationPanelProps } from '@kbn/presentation-panel-plugin/public';
import { ComparatorDefinition, StateComparators } from '@kbn/presentation-publishing';
import {
ComparatorDefinition,
StateComparators,
HasSnapshottableState,
SerializedPanelState,
} from '@kbn/presentation-publishing';
import React, { useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import { BehaviorSubject, combineLatest, debounceTime, map, skip, Subscription } from 'rxjs';
import { v4 as generateId } from 'uuid';

View file

@ -7,11 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import {
HasSerializableState,
HasSnapshottableState,
SerializedPanelState,
} from '@kbn/presentation-containers';
import { DefaultPresentationPanelApi } from '@kbn/presentation-panel-plugin/public/panel_component/types';
import {
CanLockHoverActions,
@ -19,6 +14,9 @@ import {
PublishesPhaseEvents,
PublishesUnsavedChanges,
StateComparators,
HasSerializableState,
HasSnapshottableState,
SerializedPanelState,
} from '@kbn/presentation-publishing';
import { MaybePromise } from '@kbn/utility-types';
import React from 'react';

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { SerializedPanelState } from '@kbn/presentation-containers';
import { SerializedPanelState } from '@kbn/presentation-publishing';
import { serializeState, deserializeSavedVisState } from './state';
import { VisualizeSavedVisInputState } from './types';

View file

@ -9,8 +9,7 @@
import type { SerializedSearchSourceFields } from '@kbn/data-plugin/public';
import { extractSearchSourceReferences } from '@kbn/data-plugin/public';
import { SerializedPanelState } from '@kbn/presentation-containers';
import { SerializedTitles } from '@kbn/presentation-publishing';
import { SerializedTitles, SerializedPanelState } from '@kbn/presentation-publishing';
import { cloneDeep, isEmpty, omit } from 'lodash';
import { Reference } from '../../common/content_management';
import {

View file

@ -14,6 +14,7 @@ import type { TimeRange } from '@kbn/es-query';
import { HasInspectorAdapters } from '@kbn/inspector-plugin/public';
import {
HasEditCapabilities,
HasLibraryTransforms,
HasSupportedTriggers,
PublishesDataLoading,
PublishesDataViews,
@ -98,13 +99,8 @@ export type VisualizeApi = Partial<HasEditCapabilities> &
HasInspectorAdapters &
HasSupportedTriggers &
PublishesTimeRange &
HasLibraryTransforms &
DefaultEmbeddableApi<VisualizeSerializedState, VisualizeRuntimeState> & {
updateVis: (vis: DeepPartial<SerializedVis<VisParams>>) => void;
openInspector: () => OverlayRef | undefined;
saveToLibrary: (title: string) => Promise<string>;
canLinkToLibrary: () => boolean;
canUnlinkFromLibrary: () => boolean;
checkForDuplicateTitle: (title: string) => boolean;
getByValueState: () => VisualizeSerializedState;
getByReferenceState: (id: string) => VisualizeSerializedState;
};

View file

@ -171,6 +171,22 @@ export const getVisualizeEmbeddableFactory: (deps: {
const defaultPanelTitle = new BehaviorSubject<string | undefined>(initialVisInstance.title);
const serializeVisualizeEmbeddable = (
savedObjectId: string | undefined,
linkedToLibrary: boolean
) => {
const savedObjectProperties = savedObjectProperties$.getValue();
return serializeState({
serializedVis: vis$.getValue().serialize(),
titles: serializeTitles(),
id: savedObjectId,
linkedToLibrary,
...(savedObjectProperties ? { savedObjectProperties } : {}),
...(dynamicActionsApi?.serializeDynamicActions?.() ?? {}),
...serializeCustomTimeRange(),
});
};
const api = buildApi(
{
...customTimeRangeApi,
@ -186,20 +202,13 @@ export const getVisualizeEmbeddableFactory: (deps: {
SELECT_RANGE_TRIGGER,
],
serializeState: () => {
const savedObjectProperties = savedObjectProperties$.getValue();
return serializeState({
serializedVis: vis$.getValue().serialize(),
titles: serializeTitles(),
id: savedObjectId$.getValue(),
linkedToLibrary:
// In the visualize editor, linkedToLibrary should always be false to force the full state to be serialized,
// instead of just passing a reference to the linked saved object. Other contexts like dashboards should
// serialize the state with just the savedObjectId so that the current revision of the vis is always used
apiIsOfType(parentApi, VISUALIZE_APP_NAME) ? false : linkedToLibrary$.getValue(),
...(savedObjectProperties ? { savedObjectProperties } : {}),
...(dynamicActionsApi?.serializeDynamicActions?.() ?? {}),
...serializeCustomTimeRange(),
});
// In the visualize editor, linkedToLibrary should always be false to force the full state to be serialized,
// instead of just passing a reference to the linked saved object. Other contexts like dashboards should
// serialize the state with just the savedObjectId so that the current revision of the vis is always used
const linkedToLibrary = apiIsOfType(parentApi, VISUALIZE_APP_NAME)
? false
: linkedToLibrary$.getValue();
return serializeVisualizeEmbeddable(savedObjectId$.getValue(), Boolean(linkedToLibrary));
},
getVis: () => vis$.getValue(),
getInspectorAdapters: () => inspectorAdapters$.getValue(),
@ -260,20 +269,11 @@ export const getVisualizeEmbeddableFactory: (deps: {
references,
});
},
canLinkToLibrary: () => !state.linkedToLibrary,
canUnlinkFromLibrary: () => !!state.linkedToLibrary,
checkForDuplicateTitle: () => false, // Handled by saveToLibrary action
getByValueState: () => ({
savedVis: vis$.getValue().serialize(),
...serializeTitles(),
}),
getByReferenceState: (libraryId) =>
serializeState({
serializedVis: vis$.getValue().serialize(),
titles: serializeTitles(),
id: libraryId,
linkedToLibrary: true,
}).rawState,
canLinkToLibrary: () => Promise.resolve(!state.linkedToLibrary),
canUnlinkFromLibrary: () => Promise.resolve(!!state.linkedToLibrary),
checkForDuplicateTitle: () => Promise.resolve(), // Handled by saveToLibrary action
getSerializedStateByValue: () => serializeVisualizeEmbeddable(undefined, false),
getSerializedStateByReference: (libraryId) => serializeVisualizeEmbeddable(libraryId, true),
},
{
...titleComparators,

View file

@ -77,7 +77,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await common.waitForSaveModalToClose();
await testSubjects.exists('addObjectToDashboardSuccess');
await testSubjects.existOrFail('links--component');
await dashboardPanelActions.expectLinkedToLibrary(LINKS_PANEL_NAME, false);
await dashboardPanelActions.expectLinkedToLibrary(LINKS_PANEL_NAME);
expect(await dashboardLinks.getNumberOfLinksInPanel()).to.equal(4);
await dashboard.clickDiscardChanges();

View file

@ -202,7 +202,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const panelCount = await dashboard.getPanelCount();
expect(panelCount).to.eql(2);
await dashboardPanelActions.expectLinkedToLibrary('My Saved New Vis 2', false);
await dashboardPanelActions.expectLinkedToLibrary('My Saved New Vis 2');
});
it('adding a existing metric to an existing dashboard by value', async function () {

View file

@ -19,9 +19,7 @@ const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_P
const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon';
const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector';
const COPY_PANEL_TO_DATA_TEST_SUBJ = 'embeddablePanelAction-copyToDashboard';
const LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-legacySaveToLibrary';
const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary';
const LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-legacyUnlinkFromLibrary';
const UNLINK_FROM_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-unlinkFromLibrary';
const CONVERT_TO_LENS_TEST_SUBJ = 'embeddablePanelAction-ACTION_EDIT_IN_LENS';
@ -240,13 +238,6 @@ export class DashboardPanelActionsService extends FtrService {
return response;
}
async legacyUnlinkFromLibrary(title = '') {
this.log.debug(`legacyUnlinkFromLibrary(${title}`);
await this.clickPanelActionByTitle(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, title);
await this.testSubjects.existOrFail('unlinkPanelSuccess');
await this.expectNotLinkedToLibrary(title, true);
}
async unlinkFromLibrary(title = '') {
this.log.debug(`unlinkFromLibrary(${title})`);
await this.clickPanelActionByTitle(UNLINK_FROM_LIBRARY_TEST_SUBJ, title);
@ -254,17 +245,6 @@ export class DashboardPanelActionsService extends FtrService {
await this.expectNotLinkedToLibrary(title);
}
async legacySaveToLibrary(newTitle = '', oldTitle = '') {
this.log.debug(`legacySaveToLibrary(${newTitle},${oldTitle})`);
await this.clickPanelActionByTitle(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, oldTitle);
await this.testSubjects.setValue('savedObjectTitle', newTitle, {
clearWithKeyboard: true,
});
await this.testSubjects.clickWhenNotDisabledWithoutRetry('confirmSaveSavedObjectButton');
await this.testSubjects.existOrFail('addPanelToLibrarySuccess');
await this.expectLinkedToLibrary(newTitle, true);
}
async saveToLibrary(newTitle = '', oldTitle = '') {
this.log.debug(`saveToLibraryByTitle(${newTitle},${oldTitle})`);
await this.clickPanelActionByTitle(SAVE_TO_LIBRARY_TEST_SUBJ, oldTitle);
@ -413,27 +393,19 @@ export class DashboardPanelActionsService extends FtrService {
return await this.convertToLens(wrapper);
}
async expectLinkedToLibrary(title = '', legacy?: boolean) {
async expectLinkedToLibrary(title = '') {
this.log.debug(`expectLinkedToLibrary(${title})`);
const isViewMode = await this.dashboard.getIsInViewMode();
if (isViewMode) await this.dashboard.switchToEditMode();
if (legacy) {
await this.expectExistsPanelAction(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, title);
} else {
await this.expectExistsPanelAction(UNLINK_FROM_LIBRARY_TEST_SUBJ, title);
}
await this.expectExistsPanelAction(UNLINK_FROM_LIBRARY_TEST_SUBJ, title);
if (isViewMode) await this.dashboard.clickCancelOutOfEditMode();
}
async expectNotLinkedToLibrary(title = '', legacy?: boolean) {
async expectNotLinkedToLibrary(title = '') {
this.log.debug(`expectNotLinkedToLibrary(${title})`);
const isViewMode = await this.dashboard.getIsInViewMode();
if (isViewMode) await this.dashboard.switchToEditMode();
if (legacy) {
await this.expectExistsPanelAction(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, title);
} else {
await this.expectExistsPanelAction(SAVE_TO_LIBRARY_TEST_SUBJ, title);
}
await this.expectExistsPanelAction(SAVE_TO_LIBRARY_TEST_SUBJ, title);
if (isViewMode) await this.dashboard.clickCancelOutOfEditMode();
}
}

View file

@ -7,7 +7,6 @@
import { noop } from 'lodash';
import {
HasInPlaceLibraryTransforms,
HasLibraryTransforms,
PublishesWritablePanelTitle,
PublishesWritablePanelDescription,
@ -28,6 +27,7 @@ import type {
IntegrationCallbacks,
LensInternalApi,
LensApi,
LensSerializedState,
} from '../types';
import { apiHasLensComponentProps } from '../type_guards';
import { StateManagementConfig } from './initialize_state_management';
@ -38,8 +38,7 @@ type SerializedProps = SerializedTitles & LensPanelProps & LensOverrides & LensS
export interface DashboardServicesConfig {
api: PublishesWritablePanelTitle &
PublishesWritablePanelDescription &
HasInPlaceLibraryTransforms &
HasLibraryTransforms<LensRuntimeState> &
HasLibraryTransforms<LensSerializedState, LensSerializedState> &
Pick<LensApi, 'parentApi'> &
Pick<IntegrationCallbacks, 'updateOverrides' | 'getTriggerCompatibleActions'>;
serialize: () => SerializedProps;
@ -86,10 +85,10 @@ export function initializeDashboardServices(
defaultPanelTitle: defaultPanelTitle$,
defaultPanelDescription: defaultPanelDescription$,
...titlesApi,
libraryId$: stateConfig.api.savedObjectId,
updateOverrides: internalApi.updateOverrides,
getTriggerCompatibleActions: uiActions.getTriggerCompatibleActions,
// The functions below brings the HasInPlaceLibraryTransforms compliance (new interface)
// The functions below fulfill the HasLibraryTransforms interface
saveToLibrary: async (title: string) => {
const { attributes } = getLatestState();
const savedObjectId = await attributeService.saveToLibrary(
@ -122,28 +121,14 @@ export function initializeDashboardServices(
canLinkToLibrary: async () =>
!getLatestState().savedObjectId && !isTextBasedLanguage(getLatestState()),
canUnlinkFromLibrary: async () => Boolean(getLatestState().savedObjectId),
unlinkFromLibrary: () => {
// broadcast the change to the main state serializer
stateConfig.api.updateSavedObjectId(undefined);
if ((titlesApi.panelTitle.getValue() ?? '').length === 0) {
titlesApi.setPanelTitle(defaultPanelTitle$.getValue());
}
if ((titlesApi.panelDescription.getValue() ?? '').length === 0) {
titlesApi.setPanelDescription(defaultPanelDescription$.getValue());
}
defaultPanelTitle$.next(undefined);
defaultPanelDescription$.next(undefined);
getSerializedStateByReference: (newId: string) => {
const currentState = getLatestState();
currentState.savedObjectId = newId;
return attributeService.extractReferences(currentState);
},
getByValueRuntimeSnapshot: (): Omit<LensRuntimeState, 'savedObjectId'> => {
const { savedObjectId, ...rest } = getLatestState();
return rest;
},
// The functions below brings the HasLibraryTransforms compliance (old interface)
getByReferenceState: () => getLatestState(),
getByValueState: (): Omit<LensRuntimeState, 'savedObjectId'> => {
const { savedObjectId, ...rest } = getLatestState();
return rest;
getSerializedStateByValue: () => {
const { savedObjectId, ...byValueRuntimeState } = getLatestState();
return attributeService.extractReferences(byValueRuntimeState);
},
},
serialize: () => {

View file

@ -11,7 +11,7 @@ import {
isOfAggregateQueryType,
} from '@kbn/es-query';
import { noop } from 'lodash';
import type { HasSerializableState } from '@kbn/presentation-containers';
import type { HasSerializableState } from '@kbn/presentation-publishing';
import { emptySerializer, isTextBasedLanguage } from '../helper';
import type { GetStateType, LensEmbeddableStartServices } from '../types';
import type { IntegrationCallbacks } from '../types';

View file

@ -70,14 +70,12 @@ function getDefaultLensApiMock() {
supportedTriggers: jest.fn(() => []),
canLinkToLibrary: jest.fn(async () => false),
canUnlinkFromLibrary: jest.fn(async () => false),
unlinkFromLibrary: jest.fn(),
checkForDuplicateTitle: jest.fn(),
/** New embeddable api inherited methods */
resetUnsavedChanges: jest.fn(),
serializeState: jest.fn(),
snapshotRuntimeState: jest.fn(),
saveToLibrary: jest.fn(async () => 'saved-id'),
getByValueRuntimeSnapshot: jest.fn(),
onEdit: jest.fn(),
isEditingEnabled: jest.fn(() => true),
getTypeDisplayName: jest.fn(() => 'Lens'),
@ -90,14 +88,13 @@ function getDefaultLensApiMock() {
}),
unsavedChanges: new BehaviorSubject<object | undefined>(undefined),
dataViews: new BehaviorSubject<DataView[] | undefined>(undefined),
libraryId$: new BehaviorSubject<string | undefined>(undefined),
savedObjectId: new BehaviorSubject<string | undefined>(undefined),
adapters$: new BehaviorSubject<Adapters>({}),
updateAttributes: jest.fn(),
updateSavedObjectId: jest.fn(),
updateOverrides: jest.fn(),
getByReferenceState: jest.fn(),
getByValueState: jest.fn(),
getSerializedStateByReference: jest.fn(),
getSerializedStateByValue: jest.fn(),
getTriggerCompatibleActions: jest.fn(),
blockingError: new BehaviorSubject<Error | undefined>(undefined),
panelDescription: new BehaviorSubject<string | undefined>(undefined),

View file

@ -15,7 +15,6 @@ import type {
import type { Adapters, InspectorOptions } from '@kbn/inspector-plugin/public';
import type {
HasEditCapabilities,
HasInPlaceLibraryTransforms,
HasLibraryTransforms,
HasParentApi,
HasSupportedTriggers,
@ -398,8 +397,7 @@ export type LensApi = Simplify<
HasSupportedTriggers &
PublishesDisabledActionIds &
// Offers methods to operate from/on the linked saved object
HasInPlaceLibraryTransforms &
HasLibraryTransforms<LensRuntimeState> &
HasLibraryTransforms<LensSerializedState, LensSerializedState> &
// Let the container know the view mode
PublishesViewMode &
// forward the parentApi, note that will be exposed only if it satisfy the PresentationContainer interface

View file

@ -5,8 +5,7 @@
* 2.0.
*/
import { SerializedPanelState } from '@kbn/presentation-containers';
import { HasLibraryTransforms } from '@kbn/presentation-publishing';
import { HasLibraryTransforms, SerializedPanelState } from '@kbn/presentation-publishing';
import { getCore, getCoreOverlays } from '../kibana_services';
import type { MapAttributes } from '../../common/content_management';
import { SavedMap } from '../routes/map_page';
@ -33,7 +32,7 @@ export function getByValueState(state: MapSerializedState | undefined, attribute
export function initializeLibraryTransforms(
savedMap: SavedMap,
serializeState: () => SerializedPanelState<MapSerializedState>
): HasLibraryTransforms<MapSerializedState> {
): HasLibraryTransforms<MapSerializedState, MapSerializedState> {
return {
canLinkToLibrary: async () => {
const { maps } = getCore().application.capabilities;
@ -52,8 +51,10 @@ export function initializeLibraryTransforms(
});
return savedObjectId;
},
getByReferenceState: (libraryId: string) => {
return getByReferenceState(serializeState().rawState, libraryId);
getSerializedStateByReference: (libraryId: string) => {
const { rawState: initialRawState, references } = serializeState();
const rawState = getByReferenceState(initialRawState, libraryId);
return { rawState, references };
},
checkForDuplicateTitle: async (
newTitle: string,
@ -77,8 +78,10 @@ export function initializeLibraryTransforms(
canUnlinkFromLibrary: async () => {
return savedMap.getSavedObjectId() !== undefined;
},
getByValueState: () => {
return getByValueState(serializeState().rawState, savedMap.getAttributes());
getSerializedStateByValue: () => {
const { rawState: initialRawState, references } = serializeState();
const rawState = getByValueState(initialRawState, savedMap.getAttributes());
return { rawState, references };
},
};
}

View file

@ -35,7 +35,7 @@ import { EventHandlers } from '../reducers/non_serializable_instances';
export type MapSerializedState = SerializedTitles &
Partial<DynamicActionsSerializedState> & {
// by-valye
// by-value
attributes?: MapAttributes;
// by-reference
savedObjectId?: string;
@ -61,7 +61,7 @@ export type MapApi = DefaultEmbeddableApi<MapSerializedState> &
PublishesDataLoading &
PublishesDataViews &
PublishesUnifiedSearch &
HasLibraryTransforms<MapSerializedState> & {
HasLibraryTransforms<MapSerializedState, MapSerializedState> & {
getLayerList: () => ILayer[];
reload: () => void;
setEventHandlers: (eventHandlers: EventHandlers) => void;

View file

@ -132,7 +132,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('dashboard panel - save to library', async () => {
await dashboardPanelActions.legacySaveToLibrary('', title);
await dashboardPanelActions.saveToLibrary('', title);
await a11y.testAppSnapshot();
await testSubjects.click('saveCancelButton');
});