mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
8ff18e2575
commit
3719be0144
65 changed files with 487 additions and 915 deletions
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"@kbn/es-query",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/i18n",
|
||||
"@kbn/presentation-publishing",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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> => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -9,6 +9,5 @@
|
|||
"kbn_references": [
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/core-mount-utils-browser",
|
||||
"@kbn/content-management-utils",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -21,6 +21,11 @@ describe('Clone panel action', () => {
|
|||
embeddable: {
|
||||
uuid: 'superId',
|
||||
viewMode: new BehaviorSubject<ViewMode>('edit'),
|
||||
serializeState: () => {
|
||||
return {
|
||||
rawState: {},
|
||||
};
|
||||
},
|
||||
parentApi: {
|
||||
duplicatePanel: jest.fn(),
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.",
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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',
|
|
@ -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();
|
||||
|
|
|
@ -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: () => {
|
||||
|
|
|
@ -66,6 +66,9 @@ export async function loadDashboardApi({
|
|||
...(savedObjectResult?.dashboardInput ?? {}),
|
||||
...sessionStorageInput,
|
||||
};
|
||||
combinedSessionState.references = sessionStorageInput?.references?.length
|
||||
? sessionStorageInput?.references
|
||||
: savedObjectResult?.references;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Combine state with overrides.
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue