From 7c66880a11d33adcaed65cb38e203d80a15bd7e8 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 5 Nov 2020 11:11:29 -0500 Subject: [PATCH] [Time to Visualize] Embeddable Error Handling Without ReplacePanel (#82201) Fixed embeddable error handling so that fatal errors are caught and displayed with an errorEmbeddable no matter when they occur. --- ...embeddable-public.embeddable.fatalerror.md | 11 ++++++++ ...in-plugins-embeddable-public.embeddable.md | 2 ++ ...beddable-public.embeddable.onfatalerror.md | 22 +++++++++++++++ ...mbeddable-public.iembeddable.fatalerror.md | 13 +++++++++ ...n-plugins-embeddable-public.iembeddable.md | 1 + .../actions/add_to_library_action.test.tsx | 12 ++++++++- .../actions/add_to_library_action.tsx | 4 ++- .../actions/clone_panel_action.test.tsx | 12 ++++++++- .../actions/clone_panel_action.tsx | 4 ++- .../library_notification_action.test.tsx | 12 ++++++++- .../actions/library_notification_action.tsx | 8 +++++- .../unlink_from_library_action.test.tsx | 16 ++++++++++- .../actions/unlink_from_library_action.tsx | 4 ++- .../application/dashboard_app_controller.tsx | 8 ++++-- .../public/lib/embeddables/embeddable.tsx | 15 ++++++++--- .../lib/embeddables/error_embeddable.tsx | 2 +- .../public/lib/embeddables/i_embeddable.ts | 5 ++++ .../public/lib/panel/embeddable_panel.tsx | 27 ++++++++++++++----- src/plugins/embeddable/public/public.api.md | 5 ++++ .../embeddable/embeddable.tsx | 12 +++++++-- .../lens/public/lens_attribute_service.ts | 18 ++++++------- 21 files changed, 181 insertions(+), 32 deletions(-) create mode 100644 docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.fatalerror.md create mode 100644 docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.onfatalerror.md create mode 100644 docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.fatalerror.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.fatalerror.md new file mode 100644 index 0000000000000..e937fa8fd80e7 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.fatalerror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [Embeddable](./kibana-plugin-plugins-embeddable-public.embeddable.md) > [fatalError](./kibana-plugin-plugins-embeddable-public.embeddable.fatalerror.md) + +## Embeddable.fatalError property + +Signature: + +```typescript +fatalError?: Error; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md index 295cc10b1bb19..b1f1bed7541c3 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md @@ -20,6 +20,7 @@ export declare abstract class EmbeddableError | | | [id](./kibana-plugin-plugins-embeddable-public.embeddable.id.md) | | string | | | [input](./kibana-plugin-plugins-embeddable-public.embeddable.input.md) | | TEmbeddableInput | | | [isContainer](./kibana-plugin-plugins-embeddable-public.embeddable.iscontainer.md) | | boolean | | @@ -43,6 +44,7 @@ export declare abstract class Embeddable + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [Embeddable](./kibana-plugin-plugins-embeddable-public.embeddable.md) > [onFatalError](./kibana-plugin-plugins-embeddable-public.embeddable.onfatalerror.md) + +## Embeddable.onFatalError() method + +Signature: + +```typescript +protected onFatalError(e: Error): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| e | Error | | + +Returns: + +`void` + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md new file mode 100644 index 0000000000000..4b764a6ede079 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [IEmbeddable](./kibana-plugin-plugins-embeddable-public.iembeddable.md) > [fatalError](./kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md) + +## IEmbeddable.fatalError property + +If this embeddable has encountered a fatal error, that error will be stored here + +Signature: + +```typescript +fatalError?: Error; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.md index b3b6f961e56d1..f96477ed65a04 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iembeddable.md @@ -15,6 +15,7 @@ export interface IEmbeddableobject | Extra abilities added to Embeddable by *_enhanced plugins. | +| [fatalError](./kibana-plugin-plugins-embeddable-public.iembeddable.fatalerror.md) | Error | If this embeddable has encountered a fatal error, that error will be stored here | | [id](./kibana-plugin-plugins-embeddable-public.iembeddable.id.md) | string | A unique identifier for this embeddable. Mainly only used by containers to map their Panel States to a child embeddable instance. | | [isContainer](./kibana-plugin-plugins-embeddable-public.iembeddable.iscontainer.md) | boolean | Is this embeddable an instance of a Container class, can it contain nested embeddables? | | [parent](./kibana-plugin-plugins-embeddable-public.iembeddable.parent.md) | IContainer | If this embeddable is nested inside a container, this will contain a reference to its parent. | diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx index 3f7d05e8692c2..650a273314412 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx @@ -35,7 +35,7 @@ import { coreMock } from '../../../../../core/public/mocks'; import { CoreStart } from 'kibana/public'; import { AddToLibraryAction } from '.'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; -import { ViewMode } from '../../../../embeddable/public'; +import { ErrorEmbeddable, ViewMode } from '../../../../embeddable/public'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -86,6 +86,16 @@ beforeEach(async () => { } }); +test('Add to library is incompatible with Error Embeddables', async () => { + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); +}); + test('Add to library is compatible when embeddable on dashboard has value type input', async () => { const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsValueType()); diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index d89c38f297e8f..179e5d522a2b3 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -26,6 +26,7 @@ import { PanelNotFoundError, EmbeddableInput, isReferenceOrValueEmbeddable, + isErrorEmbeddable, } from '../../../../embeddable/public'; import { NotificationsStart } from '../../../../../core/public'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; @@ -61,7 +62,8 @@ export class AddToLibraryAction implements ActionByType { } }); +test('Clone is incompatible with Error Embeddables', async () => { + const action = new ClonePanelAction(coreStart); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); +}); + test('Clone adds a new embeddable', async () => { const dashboard = embeddable.getRoot() as IContainer; const originalPanelCount = Object.keys(dashboard.getInput().panels).length; diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index dc5887ee0e644..2d98d419689c1 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -28,6 +28,7 @@ import { PanelNotFoundError, EmbeddableInput, SavedObjectEmbeddableInput, + isErrorEmbeddable, } from '../../../../embeddable/public'; import { placePanelBeside, @@ -66,7 +67,8 @@ export class ClonePanelAction implements ActionByType public async isCompatible({ embeddable }: ClonePanelActionContext) { return Boolean( - embeddable.getInput()?.viewMode !== ViewMode.VIEW && + !isErrorEmbeddable(embeddable) && + embeddable.getInput()?.viewMode !== ViewMode.VIEW && embeddable.getRoot() && embeddable.getRoot().isContainer && embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx index 996649677e6c9..f45d64cdc0ab8 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx @@ -30,7 +30,7 @@ import { coreMock } from '../../../../../core/public/mocks'; import { CoreStart } from 'kibana/public'; import { LibraryNotificationAction, UnlinkFromLibraryAction } from '.'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; -import { ViewMode } from '../../../../embeddable/public'; +import { ErrorEmbeddable, IContainer, ViewMode } from '../../../../embeddable/public'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -87,6 +87,16 @@ beforeEach(async () => { embeddable.updateInput({ viewMode: ViewMode.EDIT }); }); +test('Notification is incompatible with Error Embeddables', async () => { + const action = new LibraryNotificationAction(unlinkAction); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); +}); + test('Notification is shown when embeddable on dashboard has reference type input', async () => { const action = new LibraryNotificationAction(unlinkAction); embeddable.updateInput(await embeddable.getInputAsRefType()); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx index 6a0b71d8250be..d6e75a3bb132b 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx @@ -19,7 +19,12 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { IEmbeddable, ViewMode, isReferenceOrValueEmbeddable } from '../../embeddable_plugin'; +import { + IEmbeddable, + ViewMode, + isReferenceOrValueEmbeddable, + isErrorEmbeddable, +} from '../../embeddable_plugin'; import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; import { reactToUiComponent } from '../../../../kibana_react/public'; import { UnlinkFromLibraryAction } from '.'; @@ -79,6 +84,7 @@ export class LibraryNotificationAction implements ActionByType { return ( + !isErrorEmbeddable(embeddable) && embeddable.getRoot().isContainer && embeddable.getInput()?.viewMode !== ViewMode.VIEW && isReferenceOrValueEmbeddable(embeddable) && diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx index 0f61a74cd7036..4f668ec9ea04c 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx @@ -30,7 +30,11 @@ import { coreMock } from '../../../../../core/public/mocks'; import { CoreStart } from 'kibana/public'; import { UnlinkFromLibraryAction } from '.'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; -import { ViewMode, SavedObjectEmbeddableInput } from '../../../../embeddable/public'; +import { + ViewMode, + SavedObjectEmbeddableInput, + ErrorEmbeddable, +} from '../../../../embeddable/public'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -80,6 +84,16 @@ beforeEach(async () => { embeddable.updateInput({ viewMode: ViewMode.EDIT }); }); +test('Unlink is incompatible with Error Embeddables', async () => { + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); +}); + test('Unlink is compatible when embeddable on dashboard has reference type input', async () => { const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsRefType()); diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx index f5cf8b4e866a8..5e16145364712 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx @@ -26,6 +26,7 @@ import { PanelNotFoundError, EmbeddableInput, isReferenceOrValueEmbeddable, + isErrorEmbeddable, } from '../../../../embeddable/public'; import { NotificationsStart } from '../../../../../core/public'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; @@ -61,7 +62,8 @@ export class UnlinkFromLibraryAction implements ActionByType merge( ...newChildIds.map((childId) => - dashboardContainer!.getChild(childId).getOutput$() + dashboardContainer! + .getChild(childId) + .getOutput$() + .pipe(catchError(() => EMPTY)) ) ) ) diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index c7afc157c1452..31df5c5085f8b 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -43,6 +43,7 @@ export abstract class Embeddable< public readonly isContainer: boolean = false; public abstract readonly type: string; public readonly id: string; + public fatalError?: Error; protected output: TEmbeddableOutput; protected input: TEmbeddableInput; @@ -88,9 +89,12 @@ export abstract class Embeddable< map(({ title }) => title || ''), distinctUntilChanged() ) - .subscribe((title) => { - this.renderComplete.setTitle(title); - }); + .subscribe( + (title) => { + this.renderComplete.setTitle(title); + }, + () => {} + ); } public getIsContainer(): this is IContainer { @@ -193,6 +197,11 @@ export abstract class Embeddable< } } + protected onFatalError(e: Error) { + this.fatalError = e; + this.output$.error(e); + } + private onResetInput(newInput: TEmbeddableInput) { if (!isEqual(this.input, newInput)) { const oldLastReloadRequestTime = this.input.lastReloadRequestTime; diff --git a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx index cdbe7af98a4f4..34d971cbb717a 100644 --- a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx @@ -30,7 +30,7 @@ export const ERROR_EMBEDDABLE_TYPE = 'error'; export function isErrorEmbeddable( embeddable: TEmbeddable | ErrorEmbeddable ): embeddable is ErrorEmbeddable { - return (embeddable as ErrorEmbeddable).error !== undefined; + return Boolean(embeddable.fatalError || (embeddable as ErrorEmbeddable).error !== undefined); } export class ErrorEmbeddable extends Embeddable { diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 3843950c164c9..5a73df2e13861 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -85,6 +85,11 @@ export interface IEmbeddable< */ enhancements?: object; + /** + * If this embeddable has encountered a fatal error, that error will be stored here + **/ + fatalError?: Error; + /** * A functional representation of the isContainer variable, but helpful for typescript to * know the shape if this returns true diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 1cd48e85469fd..4a1fd22894e7e 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -50,7 +50,7 @@ import { EditPanelAction } from '../actions'; import { CustomizePanelModal } from './panel_header/panel_actions/customize_title/customize_panel_modal'; import { EmbeddableStart } from '../../plugin'; import { EmbeddableErrorLabel } from './embeddable_error_label'; -import { EmbeddableStateTransfer } from '..'; +import { EmbeddableStateTransfer, ErrorEmbeddable } from '..'; const sortByOrderField = ( { order: orderA }: { order?: number }, @@ -85,6 +85,7 @@ interface State { notifications: Array>; loading?: boolean; error?: EmbeddableError; + errorEmbeddable?: ErrorEmbeddable; } interface PanelUniversalActions { @@ -199,6 +200,9 @@ export class EmbeddablePanel extends React.Component { if (this.parentSubscription) { this.parentSubscription.unsubscribe(); } + if (this.state.errorEmbeddable) { + this.state.errorEmbeddable.destroy(); + } this.props.embeddable.destroy(); } @@ -257,12 +261,21 @@ export class EmbeddablePanel extends React.Component { public componentDidMount() { if (this.embeddableRoot.current) { this.subscription.add( - this.props.embeddable.getOutput$().subscribe((output: EmbeddableOutput) => { - this.setState({ - error: output.error, - loading: output.loading, - }); - }) + this.props.embeddable.getOutput$().subscribe( + (output: EmbeddableOutput) => { + this.setState({ + error: output.error, + loading: output.loading, + }); + }, + (error) => { + if (this.embeddableRoot.current) { + const errorEmbeddable = new ErrorEmbeddable(error, { id: this.props.embeddable.id }); + errorEmbeddable.render(this.embeddableRoot.current); + this.setState({ errorEmbeddable }); + } + } + ) ); this.props.embeddable.render(this.embeddableRoot.current); } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 8f0f56c4e1a16..4406dded98547 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -278,6 +278,8 @@ export abstract class Embeddable>; // (undocumented) getInput(): Readonly; @@ -298,6 +300,8 @@ export abstract class Embeddable { destroy(): void; enhancements?: object; + fatalError?: Error; getInput$(): Readonly>; getInput(): Readonly; getInspectorAdapters(): Adapters | undefined; diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 02ac58328b4e0..fdb267835f44c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -44,7 +44,7 @@ import { getEditPath, DOC_TYPE } from '../../../common'; import { IBasePath } from '../../../../../../src/core/public'; import { LensAttributeService } from '../../lens_attribute_service'; -export type LensSavedObjectAttributes = Omit; +export type LensSavedObjectAttributes = Omit; export type LensByValueInput = { attributes: LensSavedObjectAttributes; @@ -130,7 +130,15 @@ export class Embeddable } async initializeSavedVis(input: LensEmbeddableInput) { - const attributes = await this.deps.attributeService.unwrapAttributes(input); + const attributes: + | LensSavedObjectAttributes + | false = await this.deps.attributeService.unwrapAttributes(input).catch((e: Error) => { + this.onFatalError(e); + return false; + }); + if (!attributes) { + return; + } this.savedVis = { ...attributes, type: this.type, diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts index fac8f445abb9e..e8bb031a2b027 100644 --- a/x-pack/plugins/lens/public/lens_attribute_service.ts +++ b/x-pack/plugins/lens/public/lens_attribute_service.ts @@ -12,7 +12,7 @@ import { LensByValueInput, LensByReferenceInput, } from './editor_frame_service/embeddable/embeddable'; -import { SavedObjectIndexStore } from './persistence'; +import { SavedObjectIndexStore, Document } from './persistence'; import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; import { DOC_TYPE } from '../common'; @@ -22,6 +22,12 @@ export type LensAttributeService = AttributeService< LensByReferenceInput >; +function documentToAttributes(doc: Document): LensSavedObjectAttributes { + delete doc.savedObjectId; + delete doc.type; + return { ...doc }; +} + export function getLensAttributeService( core: CoreStart, startDependencies: LensPluginStartDependencies @@ -41,14 +47,8 @@ export function getLensAttributeService( return { id: savedDoc.savedObjectId }; }, unwrapMethod: async (savedObjectId: string): Promise => { - const savedObject = await core.savedObjects.client.get( - DOC_TYPE, - savedObjectId - ); - return { - ...savedObject.attributes, - references: savedObject.references, - }; + const attributes = documentToAttributes(await savedObjectStore.load(savedObjectId)); + return attributes; }, checkForDuplicateTitle: (props: OnSaveProps) => { const savedObjectsClient = core.savedObjects.client;