[embeddables] state management example (#192587)

Extend embeddable examples with a state management example. PR also
refactors embeddable examples to use side nav instead of tabs.

<img width="800" alt="Screenshot 2024-09-11 at 8 38 28 AM"
src="https://github.com/user-attachments/assets/ac46600f-2c45-4f9e-b4f8-a5c03f4eef2f">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2024-09-12 09:50:27 -06:00 committed by GitHub
parent 8d90d989d7
commit 32c9913c2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 401 additions and 141 deletions

View file

@ -7,107 +7,98 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useState } from 'react';
import React, { useMemo } from 'react';
import { Redirect } from 'react-router-dom';
import ReactDOM from 'react-dom';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import {
EuiPage,
EuiPageBody,
EuiPageHeader,
EuiPageSection,
EuiPageTemplate,
EuiSpacer,
EuiTab,
EuiTabs,
} from '@elastic/eui';
import { BrowserRouter as Router, Routes, Route } from '@kbn/shared-ux-router';
import { EuiPageTemplate, EuiTitle } from '@elastic/eui';
import { Overview } from './overview';
import { RegisterEmbeddable } from './register_embeddable';
import { RenderExamples } from './render_examples';
import { PresentationContainerExample } from './presentation_container_example/components/presentation_container_example';
import { StartDeps } from '../plugin';
import { Sidebar } from './sidebar';
import { StateManagementExample } from './state_management_example/state_management_example';
const OVERVIEW_TAB_ID = 'overview';
const REGISTER_EMBEDDABLE_TAB_ID = 'register';
const RENDER_TAB_ID = 'render';
const PRESENTATION_CONTAINER_EXAMPLE_ID = 'presentationContainerExample';
const App = ({
core,
deps,
mountParams,
}: {
core: CoreStart;
deps: StartDeps;
mountParams: AppMountParameters;
}) => {
const pages = useMemo(() => {
return [
{
id: 'overview',
title: 'Embeddables overview',
component: <Overview />,
},
{
id: 'registerEmbeddable',
title: 'Register a new embeddable type',
component: <RegisterEmbeddable />,
},
{
id: 'renderEmbeddable',
title: 'Render embeddables in your application',
component: <RenderExamples />,
},
{
id: 'stateManagement',
title: 'Embeddable state management',
component: <StateManagementExample uiActions={deps.uiActions} />,
},
{
id: 'presentationContainer',
title: 'Create a dashboard like experience with embeddables',
component: <PresentationContainerExample uiActions={deps.uiActions} />,
},
];
}, [deps.uiActions]);
const App = ({ core, deps }: { core: CoreStart; deps: StartDeps }) => {
const [selectedTabId, setSelectedTabId] = useState(OVERVIEW_TAB_ID);
function onSelectedTabChanged(tabId: string) {
setSelectedTabId(tabId);
}
function renderTabContent() {
if (selectedTabId === RENDER_TAB_ID) {
return <RenderExamples />;
}
if (selectedTabId === REGISTER_EMBEDDABLE_TAB_ID) {
return <RegisterEmbeddable />;
}
if (selectedTabId === PRESENTATION_CONTAINER_EXAMPLE_ID) {
return <PresentationContainerExample uiActions={deps.uiActions} />;
}
return <Overview />;
}
const routes = useMemo(() => {
return pages.map((page) => (
<Route
key={page.id}
path={`/${page.id}`}
render={(props) => (
<>
<EuiPageTemplate.Header>
<EuiTitle size="l">
<h1 data-test-subj="responseStreamPageTitle">{page.title}</h1>
</EuiTitle>
</EuiPageTemplate.Header>
<EuiPageTemplate.Section>{page.component}</EuiPageTemplate.Section>
</>
)}
/>
));
}, [pages]);
return (
<KibanaRenderContextProvider i18n={core.i18n} theme={core.theme}>
<EuiPage>
<EuiPageBody>
<EuiPageSection>
<EuiPageHeader pageTitle="Embeddables" />
</EuiPageSection>
<EuiPageTemplate.Section>
<EuiPageSection>
<EuiTabs>
<EuiTab
onClick={() => onSelectedTabChanged(OVERVIEW_TAB_ID)}
isSelected={OVERVIEW_TAB_ID === selectedTabId}
>
Embeddables overview
</EuiTab>
<EuiTab
onClick={() => onSelectedTabChanged(REGISTER_EMBEDDABLE_TAB_ID)}
isSelected={REGISTER_EMBEDDABLE_TAB_ID === selectedTabId}
>
Register new embeddable type
</EuiTab>
<EuiTab
onClick={() => onSelectedTabChanged(RENDER_TAB_ID)}
isSelected={RENDER_TAB_ID === selectedTabId}
>
Rendering embeddables in your application
</EuiTab>
<EuiTab
onClick={() => onSelectedTabChanged(PRESENTATION_CONTAINER_EXAMPLE_ID)}
isSelected={PRESENTATION_CONTAINER_EXAMPLE_ID === selectedTabId}
>
PresentationContainer example
</EuiTab>
</EuiTabs>
<EuiSpacer />
{renderTabContent()}
</EuiPageSection>
</EuiPageTemplate.Section>
</EuiPageBody>
</EuiPage>
<Router basename={mountParams.appBasePath}>
<EuiPageTemplate restrictWidth={true} offset={0}>
<EuiPageTemplate.Sidebar sticky={true}>
<Sidebar pages={pages} />
</EuiPageTemplate.Sidebar>
<Routes>
{routes}
<Redirect to="/overview" />
</Routes>
</EuiPageTemplate>
</Router>
</KibanaRenderContextProvider>
);
};
export const renderApp = (
core: CoreStart,
deps: StartDeps,
element: AppMountParameters['element']
) => {
ReactDOM.render(<App core={core} deps={deps} />, element);
export const renderApp = (core: CoreStart, deps: StartDeps, mountParams: AppMountParameters) => {
ReactDOM.render(<App core={core} deps={deps} mountParams={mountParams} />, mountParams.element);
return () => ReactDOM.unmountComponentAtNode(element);
return () => ReactDOM.unmountComponentAtNode(mountParams.element);
};

View file

@ -10,15 +10,9 @@
import React, { ReactElement, useEffect, useState } from 'react';
import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { ADD_PANEL_TRIGGER, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { ParentApi } from '../types';
import { PageApi } from '../types';
export function AddButton({
parentApi,
uiActions,
}: {
parentApi: ParentApi;
uiActions: UiActionsStart;
}) {
export function AddButton({ pageApi, uiActions }: { pageApi: PageApi; uiActions: UiActionsStart }) {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [items, setItems] = useState<ReactElement[]>([]);
@ -26,7 +20,7 @@ export function AddButton({
let cancelled = false;
const actionContext = {
embeddable: parentApi,
embeddable: pageApi,
trigger: {
id: ADD_PANEL_TRIGGER,
},
@ -67,7 +61,7 @@ export function AddButton({
return () => {
cancelled = true;
};
}, [parentApi, uiActions]);
}, [pageApi, uiActions]);
return (
<EuiPopover

View file

@ -19,15 +19,15 @@ import {
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { getParentApi } from '../parent_api';
import { getPageApi } from '../page_api';
import { AddButton } from './add_button';
import { TopNav } from './top_nav';
import { lastSavedStateSessionStorage } from '../session_storage/last_saved_state';
import { unsavedChangesSessionStorage } from '../session_storage/unsaved_changes';
export const PresentationContainerExample = ({ uiActions }: { uiActions: UiActionsStart }) => {
const { cleanUp, componentApi, parentApi } = useMemo(() => {
return getParentApi();
const { cleanUp, componentApi, pageApi } = useMemo(() => {
return getPageApi();
}, []);
useEffect(() => {
@ -37,25 +37,23 @@ export const PresentationContainerExample = ({ uiActions }: { uiActions: UiActio
}, [cleanUp]);
const [dataLoading, panels, timeRange] = useBatchedPublishingSubjects(
parentApi.dataLoading,
pageApi.dataLoading,
componentApi.panels$,
parentApi.timeRange$
pageApi.timeRange$
);
return (
<div>
<EuiCallOut title="Presentation Container interfaces">
<p>
At times, you will need to render many embeddables and allow users to add, remove, and
re-arrange embeddables. Use the <strong>PresentationContainer</strong> and{' '}
At times, you will need to render many embeddables and allow users to add and remove
embeddables. Use the <strong>PresentationContainer</strong> and{' '}
<strong>CanAddNewPanel</strong> interfaces for this functionallity.
</p>
<p>
Each embeddable manages its own state. The page is only responsible for persisting and
providing the last persisted state to the embeddable. Implement{' '}
<strong>HasSerializedChildState</strong> interface to provide an embeddable with last
persisted state. Implement <strong>HasRuntimeChildState</strong> interface to provide an
embeddable with a previous session&apos;s unsaved changes.
New embeddable state is provided to the page by calling{' '}
<strong>pageApi.addNewPanel</strong>. The page provides new embeddable state to the
embeddable with <strong>pageApi.getRuntimeStateForChild</strong>.
</p>
<p>
This example uses session storage to persist saved state and unsaved changes while a
@ -96,8 +94,8 @@ export const PresentationContainerExample = ({ uiActions }: { uiActions: UiActio
<EuiFlexItem grow={false}>
<TopNav
onSave={componentApi.onSave}
resetUnsavedChanges={parentApi.resetUnsavedChanges}
unsavedChanges$={parentApi.unsavedChanges}
resetUnsavedChanges={pageApi.resetUnsavedChanges}
unsavedChanges$={pageApi.unsavedChanges}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -110,7 +108,7 @@ export const PresentationContainerExample = ({ uiActions }: { uiActions: UiActio
<ReactEmbeddableRenderer
type={type}
maybeId={id}
getParentApi={() => parentApi}
getParentApi={() => pageApi}
hidePanelChrome={false}
onApiAvailable={(api) => {
componentApi.setChild(id, api);
@ -121,7 +119,7 @@ export const PresentationContainerExample = ({ uiActions }: { uiActions: UiActio
);
})}
<AddButton parentApi={parentApi} uiActions={uiActions} />
<AddButton pageApi={pageApi} uiActions={uiActions} />
</div>
);
};

View file

@ -27,9 +27,9 @@ import {
} from '@kbn/presentation-publishing';
import { DEFAULT_STATE, lastSavedStateSessionStorage } from './session_storage/last_saved_state';
import { unsavedChangesSessionStorage } from './session_storage/unsaved_changes';
import { LastSavedState, ParentApi, UnsavedChanges } from './types';
import { LastSavedState, PageApi, UnsavedChanges } from './types';
export function getParentApi() {
export function getPageApi() {
const initialUnsavedChanges = unsavedChangesSessionStorage.load();
const initialSavedState = lastSavedStateSessionStorage.load();
let newPanels: Record<string, object> = {};
@ -185,7 +185,7 @@ export function getParentApi() {
timeRange$.next(timeRange);
},
},
parentApi: {
pageApi: {
addNewPanel: async ({ panelType, initialState }: PanelPackage) => {
const id = generateId();
panels$.next([...panels$.value, { id, type: panelType }]);
@ -257,6 +257,6 @@ export function getParentApi() {
},
timeRange$,
unsavedChanges: unsavedChanges$ as PublishingSubject<object | undefined>,
} as ParentApi,
} as PageApi,
};
}

View file

@ -26,7 +26,7 @@ import {
} from '@kbn/presentation-publishing';
import { PublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/publishes_reload';
export type ParentApi = PresentationContainer &
export type PageApi = PresentationContainer &
CanAddNewPanel &
HasExecutionContext &
HasSaveNotification &

View file

@ -19,10 +19,10 @@ export function setupApp(core: CoreSetup<StartDeps>, developerExamples: Develope
id: APP_ID,
title,
visibleIn: [],
async mount(params: AppMountParameters) {
async mount(mountParams: AppMountParameters) {
const { renderApp } = await import('./app');
const [coreStart, deps] = await core.getStartServices();
return renderApp(coreStart, deps, params.element);
return renderApp(coreStart, deps, mountParams);
},
});
developerExamples.register({

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", 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 React, { useMemo } from 'react';
import { css } from '@emotion/react';
import { useHistory } from 'react-router-dom';
import { EuiSideNav } from '@elastic/eui';
export function Sidebar({ pages }: { pages: Array<{ id: string; title: string }> }) {
const history = useHistory();
const items = useMemo(() => {
return pages.map((page) => {
return {
id: page.id,
name: page.title,
onClick: () => history.push(`/${page.id}`),
};
});
}, [pages, history]);
return (
<EuiSideNav
css={{
css: css`
maxwidth: '85%';
`,
}}
truncate={false}
items={items}
/>
);
}

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", 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 { SerializedPanelState } from '@kbn/presentation-containers';
import { BookSerializedState } from '../../react_embeddables/saved_book/types';
const SAVED_STATE_SESSION_STORAGE_KEY =
'kibana.examples.embeddables.stateManagementExample.savedState';
export const lastSavedStateSessionStorage = {
clear: () => {
sessionStorage.removeItem(SAVED_STATE_SESSION_STORAGE_KEY);
},
load: (): SerializedPanelState<BookSerializedState> | undefined => {
const savedState = sessionStorage.getItem(SAVED_STATE_SESSION_STORAGE_KEY);
return savedState ? JSON.parse(savedState) : undefined;
},
save: (state: SerializedPanelState<BookSerializedState>) => {
sessionStorage.setItem(SAVED_STATE_SESSION_STORAGE_KEY, JSON.stringify(state));
},
};

View file

@ -0,0 +1,175 @@
/*
* 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 React, { useEffect, useMemo, useState } from 'react';
import {
EuiBadge,
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
} from '@elastic/eui';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { ViewMode } from '@kbn/presentation-publishing';
import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
import { BehaviorSubject, Subject } from 'rxjs';
import useMountedState from 'react-use/lib/useMountedState';
import { SAVED_BOOK_ID } from '../../react_embeddables/saved_book/constants';
import {
BookApi,
BookRuntimeState,
BookSerializedState,
} from '../../react_embeddables/saved_book/types';
import { lastSavedStateSessionStorage } from './last_saved_state';
import { unsavedChangesSessionStorage } from './unsaved_changes';
export const StateManagementExample = ({ uiActions }: { uiActions: UiActionsStart }) => {
const isMounted = useMountedState();
const saveNotification$ = useMemo(() => {
return new Subject<void>();
}, []);
const [bookApi, setBookApi] = useState<BookApi | undefined>();
const [isSaving, setIsSaving] = useState(false);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
useEffect(() => {
if (!bookApi || !bookApi.unsavedChanges) {
return;
}
const subscription = bookApi.unsavedChanges.subscribe((unsavedChanges) => {
setHasUnsavedChanges(unsavedChanges !== undefined);
unsavedChangesSessionStorage.save(unsavedChanges ?? {});
});
return () => {
subscription.unsubscribe();
};
}, [bookApi]);
return (
<div>
<EuiCallOut>
<p>
Each embeddable manages its own state. The page is only responsible for persisting and
providing the last persisted state to the embeddable.
</p>
<p>
The page renders the embeddable with <strong>ReactEmbeddableRenderer</strong> component.
On mount, ReactEmbeddableRenderer component calls{' '}
<strong>pageApi.getSerializedStateForChild</strong> to get the last saved state.
ReactEmbeddableRenderer component then calls{' '}
<strong>pageApi.getRuntimeStateForChild</strong> to get the last session&apos;s unsaved
changes. ReactEmbeddableRenderer merges last saved state with unsaved changes and passes
the merged state to the embeddable factory. ReactEmbeddableRender passes the embeddableApi
to the page by calling <strong>onApiAvailable</strong>.
</p>
<p>
The page subscribes to <strong>embeddableApi.unsavedChanges</strong> to receive embeddable
unsaved changes. The page persists unsaved changes in session storage. The page provides
unsaved changes to the embeddable with <strong>pageApi.getRuntimeStateForChild</strong>.
</p>
<p>
The page gets embeddable state by calling <strong>embeddableApi.serializeState</strong>.
The page persists embeddable state in session storage. The page provides last saved state
to the embeddable with <strong>pageApi.getSerializedStateForChild</strong>.
</p>
<p>
<EuiButtonEmpty
color={'warning'}
onClick={() => {
lastSavedStateSessionStorage.clear();
unsavedChangesSessionStorage.clear();
window.location.reload();
}}
>
Reset example
</EuiButtonEmpty>
</p>
</EuiCallOut>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexEnd">
{hasUnsavedChanges && (
<>
<EuiFlexItem grow={false}>
<EuiBadge color="warning">Unsaved changes</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
disabled={isSaving || !bookApi}
onClick={() => {
bookApi?.resetUnsavedChanges?.();
}}
>
Reset
</EuiButtonEmpty>
</EuiFlexItem>
</>
)}
<EuiFlexItem grow={false}>
<EuiButton
disabled={isSaving || !hasUnsavedChanges}
onClick={async () => {
if (!bookApi) {
return;
}
setIsSaving(true);
const bookSerializedState = await bookApi.serializeState();
if (!isMounted()) {
return;
}
lastSavedStateSessionStorage.save(bookSerializedState);
saveNotification$.next(); // signals embeddable unsaved change tracking to update last saved state
setHasUnsavedChanges(false);
setIsSaving(false);
}}
>
Save
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<ReactEmbeddableRenderer<BookSerializedState, BookRuntimeState, BookApi>
type={SAVED_BOOK_ID}
getParentApi={() => {
return {
/**
* return last saved embeddable state
*/
getSerializedStateForChild: (childId: string) => {
return lastSavedStateSessionStorage.load();
},
/**
* return previous session's unsaved changes for embeddable
*/
getRuntimeStateForChild: (childId: string) => {
return unsavedChangesSessionStorage.load();
},
saveNotification$,
viewMode: new BehaviorSubject<ViewMode>('edit'),
};
}}
onApiAvailable={(api) => {
setBookApi(api);
}}
/>
</div>
);
};

View file

@ -0,0 +1,26 @@
/*
* 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 { BookRuntimeState } from '../../react_embeddables/saved_book/types';
const UNSAVED_CHANGES_SESSION_STORAGE_KEY =
'kibana.examples.embeddables.stateManagementExample.unsavedChanges';
export const unsavedChangesSessionStorage = {
clear: () => {
sessionStorage.removeItem(UNSAVED_CHANGES_SESSION_STORAGE_KEY);
},
load: (): Partial<BookRuntimeState> | undefined => {
const unsavedChanges = sessionStorage.getItem(UNSAVED_CHANGES_SESSION_STORAGE_KEY);
return unsavedChanges ? JSON.parse(unsavedChanges) : undefined;
},
save: (unsavedChanges: Partial<BookRuntimeState>) => {
sessionStorage.setItem(UNSAVED_CHANGES_SESSION_STORAGE_KEY, JSON.stringify(unsavedChanges));
},
};

View file

@ -9,7 +9,7 @@
import { CoreStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
import { apiCanAddNewPanel } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { UiActionsPublicStart } from '@kbn/ui-actions-plugin/public/plugin';
@ -30,10 +30,10 @@ export const registerCreateSavedBookAction = (uiActions: UiActionsPublicStart, c
getIconType: () => 'folderClosed',
grouping: [embeddableExamplesGrouping],
isCompatible: async ({ embeddable }) => {
return apiIsPresentationContainer(embeddable);
return apiCanAddNewPanel(embeddable);
},
execute: async ({ embeddable }) => {
if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError();
if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError();
const newPanelStateManager = stateManagerFromAttributes(defaultBookAttributes);
const { addToLibrary } = await openSavedBookEditor(newPanelStateManager, true, core, {

View file

@ -135,7 +135,7 @@ export const SavedBookEditor = ({
})}
>
<EuiFieldText
value={authorName}
value={authorName ?? ''}
onChange={(e) => attributesManager.authorName.next(e.target.value)}
/>
</EuiFormRow>
@ -145,7 +145,7 @@ export const SavedBookEditor = ({
})}
>
<EuiFieldText
value={bookTitle}
value={bookTitle ?? ''}
onChange={(e) => attributesManager.bookTitle.next(e.target.value)}
/>
</EuiFormRow>
@ -155,7 +155,7 @@ export const SavedBookEditor = ({
})}
>
<EuiFieldNumber
value={numberOfPages}
value={numberOfPages ?? ''}
onChange={(e) => attributesManager.numberOfPages.next(+e.target.value)}
/>
</EuiFormRow>
@ -165,7 +165,7 @@ export const SavedBookEditor = ({
})}
>
<EuiTextArea
value={synopsis}
value={synopsis ?? ''}
onChange={(e) => attributesManager.bookSynopsis.next(e.target.value)}
/>
</EuiFormRow>

View file

@ -13,6 +13,7 @@ import { CoreStart } from '@kbn/core-lifecycle-browser';
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import {
apiHasParentApi,
initializeTitles,
SerializedTitles,
useBatchedPublishingSubjects,
@ -20,6 +21,7 @@ import {
import { euiThemeVars } from '@kbn/ui-theme';
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';
import { openSavedBookEditor } from './saved_book_editor';
@ -137,6 +139,10 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => {
}
);
const showLibraryCallout =
apiHasParentApi(api) &&
typeof (api.parentApi as PresentationContainer)?.replacePanel === 'function';
return {
api,
Component: () => {
@ -155,20 +161,22 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => {
width: 100%;
`}
>
<EuiCallOut
size="s"
color={'warning'}
title={
savedBookId
? i18n.translate('embeddableExamples.savedBook.libraryCallout', {
defaultMessage: 'Saved in library',
})
: i18n.translate('embeddableExamples.savedBook.noLibraryCallout', {
defaultMessage: 'Not saved in library',
})
}
iconType={savedBookId ? 'folderCheck' : 'folderClosed'}
/>
{showLibraryCallout && (
<EuiCallOut
size="s"
color={'warning'}
title={
savedBookId
? i18n.translate('embeddableExamples.savedBook.libraryCallout', {
defaultMessage: 'Saved in library',
})
: i18n.translate('embeddableExamples.savedBook.noLibraryCallout', {
defaultMessage: 'Not saved in library',
})
}
iconType={savedBookId ? 'folderCheck' : 'folderClosed'}
/>
)}
<div
css={css`
padding: ${euiThemeVars.euiSizeM};

View file

@ -40,6 +40,7 @@
"@kbn/kibana-utils-plugin",
"@kbn/core-mount-utils-browser",
"@kbn/react-kibana-mount",
"@kbn/std"
"@kbn/std",
"@kbn/shared-ux-router"
]
}

View file

@ -9,7 +9,7 @@ Kibana is a React application, and the minimum unit of sharing is the React comp
Rather than an inheritance-based system with classes, imperative APIs are plain old typescript objects that implement any number of shared interfaces. Interfaces are enforced via type guards and are shared via Packages.
#### Internal state management
Each embeddable manages its own state. This is because the embeddable system allows a page to render a registry of embeddable types that can change over time. This makes it untenable for a single page to manage state for every type of embeddable. The page is only responsible for persisting and providing the last persisted state to the embeddable on startup.
Each embeddable manages its own state. This is because the embeddable system allows a page to render a registry of embeddable types that can change over time. This makes it untenable for a single page to manage state for every type of embeddable. The page is only responsible for persisting and providing the last persisted state to the embeddable on startup. For implementation details, see [Embeddable state management example](https://github.com/elastic/kibana/blob/main/examples/embeddable_examples/public/app/state_management_example/state_management_example.tsx).
### Key concepts
@ -61,4 +61,5 @@ Use the following examples to learn how to create new Embeddable types. To acces
Use the following examples to render embeddables in your application. To run embeddable examples, navigate to `http://localhost:5601/app/embeddablesApp`
- [Render a single embeddable](https://github.com/elastic/kibana/blob/main/examples/embeddable_examples/public/react_embeddables/search/search_embeddable_renderer.tsx)
- [Embeddable state management](https://github.com/elastic/kibana/blob/main/examples/embeddable_examples/public/app/state_management_example/state_management_example.tsx)
- [Create a dashboard like application that renders many embeddables and allows users to add and remove embeddables](https://github.com/elastic/kibana/blob/main/examples/embeddable_examples/public/app/presentation_container_example/components/presentation_container_example.tsx)