Skip to content

Commit

Permalink
[8.x] [Dashboard] [Collapsable Panels] Add embeddable support (#198413)…
Browse files Browse the repository at this point in the history
… (#203652)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Dashboard] [Collapsable Panels] Add embeddable support
(#198413)](#198413)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Hannah
Mudge","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-12-10T17:14:31Z","message":"[Dashboard]
[Collapsable Panels] Add embeddable support (#198413)\n\nCloses
https://github.com/elastic/kibana/issues/190379\r\n\r\n##
Summary\r\n\r\nThis PR switches the example grid layout app to render
embeddables as\r\npanels rather than the simplified mock panel we were
using previously.\r\nIn doing so, I had to add the ability for custom
panels to add a custom\r\ndrag handle via the `renderPanelContents`
callback - this required\r\nadding a `setDragHandles` callback to the
`ReactEmbeddableRenderer` that\r\ncould be passed down to the
`PresentationPanel`
component.\r\n\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/9e2c68f9-34af-4360-a978-9113701a5ea2\r\n\r\n\r\n\r\n####
New scroll behaviour\r\n\r\nIn
#201867, I introduced a
small\r\n\"ease\" to the auto-scroll effect that happens when you drag a
panel to\r\nthe top or bottom of the window. However, in that PR, I was
using the\r\n`smooth` scrolling behaviour, which unfortunately became
**very**\r\njittery once I switched to embeddables rather than simple
panels\r\n(specifically in Chrome - it worked fine in
Firefox).\r\n\r\nThe only way to prevent this jittery scroll was to
switch to the default\r\nscroll behaviour, but this lead to a very
**abrupt** stop when the\r\nscrollbar reached the top and/or bottom of
the page - so, to give the\r\nsame \"gentle\" stop that the `smooth`
scroll had, I decided to recreate\r\nthis effect by adding a slow down
\"ease\" when close to the top or bottom\r\nof the
page:\r\n\r\n\r\nhttps://github.com/user-attachments/assets/cb7bf03f-4a9e-4446-be4f-8f54c0bc88ac\r\n\r\nThis
effect is accomplished via the parabola formula `y = a(x-h)2 + k`\r\nand
can be roughly visualized with the following, which shows that
the\r\n\"speed up\" ease happens at a much slower pace than the \"slow
down\"
ease:\r\n\r\n\r\n![image](https://github.com/user-attachments/assets/02b4389c-fe78-448d-9c02-c4ec5e722d5e)\r\n\r\n\r\n\r\n\r\n####
Notes about parent changes\r\nAs I investigated improving the efficiency
of the grid layout with\r\nembeddables, one of the main things I noticed
was that the grid panel\r\nwas **always** remounted when moving a panel
from one collapsible\r\nsection to another. This lead me (and
@ThomThomson) down a rabbit hole\r\nof React-reparenting, and we
explored a few different options to see if\r\nwe could change the parent
of a component **without** having it remount.\r\n\r\nIn summary, after
various experiments and a whole bunch of research, we\r\ndetermined
that, due to the reconciliation of the React tree, this
is\r\nunfortunately impossible. So our priorities will instead have to
move to\r\nmaking the remount of `ReactEmbeddableRenderer` **as
efficient as\r\npossible** via caching, since the remount is
inevitable.\r\n\r\n### Checklist\r\n\r\n- [x] The PR description
includes the appropriate Release Notes section,\r\nand the correct
`release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n###
Identify risks\r\n\r\nThere are no risks to this PR, since the most
significant work is\r\ncontained in the `examples` plugin. Some changes
were made to the\r\npresentation panel to allow for custom drag handles,
but this isn't\r\nactually used in Dashboard - so for now, this code is
only called in the\r\nexample plugin, as
well.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"2a76fe3ee432d0b6746eae660cfe31fc71d15547","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Dashboard","Feature:Embedding","Team:Presentation","loe:medium","release_note:skip","impact:high","v9.0.0","backport:prev-minor","Project:Collapsable
Panels"],"title":"[Dashboard] [Collapsable Panels] Add embeddable
support","number":198413,"url":"https://github.com/elastic/kibana/pull/198413","mergeCommit":{"message":"[Dashboard]
[Collapsable Panels] Add embeddable support (#198413)\n\nCloses
https://github.com/elastic/kibana/issues/190379\r\n\r\n##
Summary\r\n\r\nThis PR switches the example grid layout app to render
embeddables as\r\npanels rather than the simplified mock panel we were
using previously.\r\nIn doing so, I had to add the ability for custom
panels to add a custom\r\ndrag handle via the `renderPanelContents`
callback - this required\r\nadding a `setDragHandles` callback to the
`ReactEmbeddableRenderer` that\r\ncould be passed down to the
`PresentationPanel`
component.\r\n\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/9e2c68f9-34af-4360-a978-9113701a5ea2\r\n\r\n\r\n\r\n####
New scroll behaviour\r\n\r\nIn
#201867, I introduced a
small\r\n\"ease\" to the auto-scroll effect that happens when you drag a
panel to\r\nthe top or bottom of the window. However, in that PR, I was
using the\r\n`smooth` scrolling behaviour, which unfortunately became
**very**\r\njittery once I switched to embeddables rather than simple
panels\r\n(specifically in Chrome - it worked fine in
Firefox).\r\n\r\nThe only way to prevent this jittery scroll was to
switch to the default\r\nscroll behaviour, but this lead to a very
**abrupt** stop when the\r\nscrollbar reached the top and/or bottom of
the page - so, to give the\r\nsame \"gentle\" stop that the `smooth`
scroll had, I decided to recreate\r\nthis effect by adding a slow down
\"ease\" when close to the top or bottom\r\nof the
page:\r\n\r\n\r\nhttps://github.com/user-attachments/assets/cb7bf03f-4a9e-4446-be4f-8f54c0bc88ac\r\n\r\nThis
effect is accomplished via the parabola formula `y = a(x-h)2 + k`\r\nand
can be roughly visualized with the following, which shows that
the\r\n\"speed up\" ease happens at a much slower pace than the \"slow
down\"
ease:\r\n\r\n\r\n![image](https://github.com/user-attachments/assets/02b4389c-fe78-448d-9c02-c4ec5e722d5e)\r\n\r\n\r\n\r\n\r\n####
Notes about parent changes\r\nAs I investigated improving the efficiency
of the grid layout with\r\nembeddables, one of the main things I noticed
was that the grid panel\r\nwas **always** remounted when moving a panel
from one collapsible\r\nsection to another. This lead me (and
@ThomThomson) down a rabbit hole\r\nof React-reparenting, and we
explored a few different options to see if\r\nwe could change the parent
of a component **without** having it remount.\r\n\r\nIn summary, after
various experiments and a whole bunch of research, we\r\ndetermined
that, due to the reconciliation of the React tree, this
is\r\nunfortunately impossible. So our priorities will instead have to
move to\r\nmaking the remount of `ReactEmbeddableRenderer` **as
efficient as\r\npossible** via caching, since the remount is
inevitable.\r\n\r\n### Checklist\r\n\r\n- [x] The PR description
includes the appropriate Release Notes section,\r\nand the correct
`release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n###
Identify risks\r\n\r\nThere are no risks to this PR, since the most
significant work is\r\ncontained in the `examples` plugin. Some changes
were made to the\r\npresentation panel to allow for custom drag handles,
but this isn't\r\nactually used in Dashboard - so for now, this code is
only called in the\r\nexample plugin, as
well.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"2a76fe3ee432d0b6746eae660cfe31fc71d15547"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/198413","number":198413,"mergeCommit":{"message":"[Dashboard]
[Collapsable Panels] Add embeddable support (#198413)\n\nCloses
https://github.com/elastic/kibana/issues/190379\r\n\r\n##
Summary\r\n\r\nThis PR switches the example grid layout app to render
embeddables as\r\npanels rather than the simplified mock panel we were
using previously.\r\nIn doing so, I had to add the ability for custom
panels to add a custom\r\ndrag handle via the `renderPanelContents`
callback - this required\r\nadding a `setDragHandles` callback to the
`ReactEmbeddableRenderer` that\r\ncould be passed down to the
`PresentationPanel`
component.\r\n\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/9e2c68f9-34af-4360-a978-9113701a5ea2\r\n\r\n\r\n\r\n####
New scroll behaviour\r\n\r\nIn
#201867, I introduced a
small\r\n\"ease\" to the auto-scroll effect that happens when you drag a
panel to\r\nthe top or bottom of the window. However, in that PR, I was
using the\r\n`smooth` scrolling behaviour, which unfortunately became
**very**\r\njittery once I switched to embeddables rather than simple
panels\r\n(specifically in Chrome - it worked fine in
Firefox).\r\n\r\nThe only way to prevent this jittery scroll was to
switch to the default\r\nscroll behaviour, but this lead to a very
**abrupt** stop when the\r\nscrollbar reached the top and/or bottom of
the page - so, to give the\r\nsame \"gentle\" stop that the `smooth`
scroll had, I decided to recreate\r\nthis effect by adding a slow down
\"ease\" when close to the top or bottom\r\nof the
page:\r\n\r\n\r\nhttps://github.com/user-attachments/assets/cb7bf03f-4a9e-4446-be4f-8f54c0bc88ac\r\n\r\nThis
effect is accomplished via the parabola formula `y = a(x-h)2 + k`\r\nand
can be roughly visualized with the following, which shows that
the\r\n\"speed up\" ease happens at a much slower pace than the \"slow
down\"
ease:\r\n\r\n\r\n![image](https://github.com/user-attachments/assets/02b4389c-fe78-448d-9c02-c4ec5e722d5e)\r\n\r\n\r\n\r\n\r\n####
Notes about parent changes\r\nAs I investigated improving the efficiency
of the grid layout with\r\nembeddables, one of the main things I noticed
was that the grid panel\r\nwas **always** remounted when moving a panel
from one collapsible\r\nsection to another. This lead me (and
@ThomThomson) down a rabbit hole\r\nof React-reparenting, and we
explored a few different options to see if\r\nwe could change the parent
of a component **without** having it remount.\r\n\r\nIn summary, after
various experiments and a whole bunch of research, we\r\ndetermined
that, due to the reconciliation of the React tree, this
is\r\nunfortunately impossible. So our priorities will instead have to
move to\r\nmaking the remount of `ReactEmbeddableRenderer` **as
efficient as\r\npossible** via caching, since the remount is
inevitable.\r\n\r\n### Checklist\r\n\r\n- [x] The PR description
includes the appropriate Release Notes section,\r\nand the correct
`release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n###
Identify risks\r\n\r\nThere are no risks to this PR, since the most
significant work is\r\ncontained in the `examples` plugin. Some changes
were made to the\r\npresentation panel to allow for custom drag handles,
but this isn't\r\nactually used in Dashboard - so for now, this code is
only called in the\r\nexample plugin, as
well.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"2a76fe3ee432d0b6746eae660cfe31fc71d15547"}}]}]
BACKPORT-->

Co-authored-by: Hannah Mudge <[email protected]>
  • Loading branch information
kibanamachine and Heenawter authored Dec 10, 2024
1 parent bcfa88e commit d223cd7
Show file tree
Hide file tree
Showing 27 changed files with 5,389 additions and 300 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
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 { PageApi } from '../types';

export function AddButton({ pageApi, uiActions }: { pageApi: PageApi; uiActions: UiActionsStart }) {
export function AddButton({ pageApi, uiActions }: { pageApi: unknown; uiActions: UiActionsStart }) {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [items, setItems] = useState<ReactElement[]>([]);

Expand Down Expand Up @@ -73,7 +72,7 @@ export function AddButton({ pageApi, uiActions }: { pageApi: PageApi; uiActions:
setIsPopoverOpen(!isPopoverOpen);
}}
>
Add
Add panel
</EuiButton>
}
isOpen={isPopoverOpen}
Expand Down
2 changes: 2 additions & 0 deletions examples/embeddable_examples/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@

import { EmbeddableExamplesPlugin } from './plugin';

export { AddButton as AddEmbeddableButton } from './app/presentation_container_example/components/add_button';

export const plugin = () => new EmbeddableExamplesPlugin();
2 changes: 1 addition & 1 deletion examples/grid_example/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"id": "gridExample",
"server": false,
"browser": true,
"requiredPlugins": ["developerExamples"],
"requiredPlugins": ["developerExamples", "embeddable", "uiActions", "embeddableExamples"],
"requiredBundles": []
}
}
140 changes: 59 additions & 81 deletions examples/grid_example/public/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,28 @@ import deepEqual from 'fast-deep-equal';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { combineLatest, debounceTime } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';

import {
EuiBadge,
EuiButton,
EuiButtonEmpty,
EuiButtonGroup,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiPageTemplate,
EuiProvider,
EuiSpacer,
EuiButtonGroup,
EuiButtonIcon,
} from '@elastic/eui';
import { AppMountParameters } from '@kbn/core-application-browser';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import { GridLayout, GridLayoutData, GridAccessMode } from '@kbn/grid-layout';
import { AddEmbeddableButton } from '@kbn/embeddable-examples-plugin/public';
import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
import { GridLayout, GridLayoutData } from '@kbn/grid-layout';
import { i18n } from '@kbn/i18n';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';

import { getPanelId } from './get_panel_id';
import {
clearSerializedDashboardState,
getSerializedDashboardState,
Expand All @@ -45,82 +46,69 @@ const DASHBOARD_MARGIN_SIZE = 8;
const DASHBOARD_GRID_HEIGHT = 20;
const DASHBOARD_GRID_COLUMN_COUNT = 48;

export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
export const GridExample = ({
coreStart,
uiActions,
}: {
coreStart: CoreStart;
uiActions: UiActionsStart;
}) => {
const savedState = useRef<MockSerializedDashboardState>(getSerializedDashboardState());
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
const [expandedPanelId, setExpandedPanelId] = useState<string | undefined>();
const [accessMode, setAccessMode] = useState<GridAccessMode>('EDIT');
const [currentLayout, setCurrentLayout] = useState<GridLayoutData>(
dashboardInputToGridLayout(savedState.current)
);

const mockDashboardApi = useMockDashboardApi({ savedState: savedState.current });
const [viewMode, expandedPanelId] = useBatchedPublishingSubjects(
mockDashboardApi.viewMode,
mockDashboardApi.expandedPanelId
);

useEffect(() => {
combineLatest([mockDashboardApi.panels$, mockDashboardApi.rows$])
.pipe(debounceTime(0)) // debounce to avoid subscribe being called twice when both panels$ and rows$ publish
.subscribe(([panels, rows]) => {
const hasChanges = !(
deepEqual(panels, savedState.current.panels) && deepEqual(rows, savedState.current.rows)
deepEqual(
Object.values(panels).map(({ gridData }) => ({ row: 0, ...gridData })),
Object.values(savedState.current.panels).map(({ gridData }) => ({
row: 0, // if row is undefined, then default to 0
...gridData,
}))
) && deepEqual(rows, savedState.current.rows)
);
setHasUnsavedChanges(hasChanges);
setCurrentLayout(dashboardInputToGridLayout({ panels, rows }));
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const renderBasicPanel = useCallback(
(id: string) => {
const renderPanelContents = useCallback(
(id: string, setDragHandles?: (refs: Array<HTMLElement | null>) => void) => {
const currentPanels = mockDashboardApi.panels$.getValue();

return (
<>
<div style={{ padding: 8 }}>{id}</div>
<EuiButtonEmpty
onClick={() => {
setExpandedPanelId(undefined);
mockDashboardApi.removePanel(id);
}}
>
{i18n.translate('examples.gridExample.deletePanelButton', {
defaultMessage: 'Delete panel',
})}
</EuiButtonEmpty>
<EuiButtonEmpty
onClick={async () => {
setExpandedPanelId(undefined);
const newPanelId = await getPanelId({
coreStart,
suggestion: id,
});
if (newPanelId) mockDashboardApi.replacePanel(id, newPanelId);
}}
>
{i18n.translate('examples.gridExample.replacePanelButton', {
defaultMessage: 'Replace panel',
})}
</EuiButtonEmpty>
<EuiButtonIcon
iconType={expandedPanelId ? 'minimize' : 'expand'}
onClick={() => setExpandedPanelId((expandedId) => (expandedId ? undefined : id))}
aria-label={
expandedPanelId
? i18n.translate('examples.gridExample.minimizePanel', {
defaultMessage: 'Minimize panel {id}',
values: { id },
})
: i18n.translate('examples.gridExample.maximizePanel', {
defaultMessage: 'Maximize panel {id}',
values: { id },
})
}
/>
</>
<ReactEmbeddableRenderer
key={id}
maybeId={id}
type={currentPanels[id].type}
getParentApi={() => mockDashboardApi}
panelProps={{
showBadges: true,
showBorder: true,
showNotifications: true,
showShadow: false,
setDragHandles,
}}
/>
);
},
[coreStart, mockDashboardApi, setExpandedPanelId, expandedPanelId]
[mockDashboardApi]
);

return (
<EuiProvider>
<KibanaRenderContextProvider {...coreStart}>
<EuiPageTemplate grow={false} offset={0} restrictWidth={false}>
<EuiPageTemplate.Header
iconType={'dashboardApp'}
Expand All @@ -131,7 +119,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
<EuiPageTemplate.Section
color="subdued"
contentProps={{
css: { display: 'flex', flexFlow: 'column nowrap', flexGrow: 1 },
css: { flexGrow: 1 },
}}
>
<EuiCallOut
Expand All @@ -156,20 +144,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton
onClick={async () => {
setExpandedPanelId(undefined);
const panelId = await getPanelId({
coreStart,
suggestion: uuidv4(),
});
if (panelId) mockDashboardApi.addNewPanel({ id: panelId });
}}
>
{i18n.translate('examples.gridExample.addPanelButton', {
defaultMessage: 'Add a panel',
})}
</EuiButton>
<AddEmbeddableButton pageApi={mockDashboardApi} uiActions={uiActions} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
Expand All @@ -180,24 +155,24 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
})}
options={[
{
id: 'VIEW',
id: 'view',
label: i18n.translate('examples.gridExample.viewOption', {
defaultMessage: 'View',
}),
toolTipContent:
'The layout adjusts when the window is resized. Panel interactivity, such as moving and resizing within the grid, is disabled.',
},
{
id: 'EDIT',
id: 'edit',
label: i18n.translate('examples.gridExample.editOption', {
defaultMessage: 'Edit',
}),
toolTipContent: 'The layout does not adjust when the window is resized.',
},
]}
idSelected={accessMode}
idSelected={viewMode}
onChange={(id) => {
setAccessMode(id as GridAccessMode);
mockDashboardApi.viewMode.next(id);
}}
/>
</EuiFlexItem>
Expand Down Expand Up @@ -245,32 +220,35 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
</EuiFlexGroup>
<EuiSpacer size="m" />
<GridLayout
accessMode={accessMode}
accessMode={viewMode === 'view' ? 'VIEW' : 'EDIT'}
expandedPanelId={expandedPanelId}
layout={currentLayout}
gridSettings={{
gutterSize: DASHBOARD_MARGIN_SIZE,
rowHeight: DASHBOARD_GRID_HEIGHT,
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
}}
renderPanelContents={renderBasicPanel}
renderPanelContents={renderPanelContents}
onLayoutChange={(newLayout) => {
const { panels, rows } = gridLayoutToDashboardPanelMap(newLayout);
const { panels, rows } = gridLayoutToDashboardPanelMap(
mockDashboardApi.panels$.getValue(),
newLayout
);
mockDashboardApi.panels$.next(panels);
mockDashboardApi.rows$.next(rows);
}}
/>
</EuiPageTemplate.Section>
</EuiPageTemplate>
</EuiProvider>
</KibanaRenderContextProvider>
);
};

export const renderGridExampleApp = (
element: AppMountParameters['element'],
coreStart: CoreStart
deps: { uiActions: UiActionsStart; coreStart: CoreStart }
) => {
ReactDOM.render(<GridExample coreStart={coreStart} />, element);
ReactDOM.render(<GridExample {...deps} />, element);

return () => ReactDOM.unmountComponentAtNode(element);
};
Loading

0 comments on commit d223cd7

Please sign in to comment.