mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Dashboard] [Collapsable Panels] Add embeddable support (#198413)](https://github.com/elastic/kibana/pull/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":"Heenawter@users.noreply.github.com"},"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 https://github.com/elastic/kibana/pull/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\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 <42973632+kibanamachine@users.noreply.github.com>","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 https://github.com/elastic/kibana/pull/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\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 <42973632+kibanamachine@users.noreply.github.com>","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 https://github.com/elastic/kibana/pull/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\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 <42973632+kibanamachine@users.noreply.github.com>","sha":"2a76fe3ee432d0b6746eae660cfe31fc71d15547"}}]}] BACKPORT--> Co-authored-by: Hannah Mudge <Heenawter@users.noreply.github.com>
This commit is contained in:
parent
bcfa88ec96
commit
d223cd7df5
27 changed files with 5389 additions and 300 deletions
|
@ -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[]>([]);
|
||||
|
||||
|
@ -73,7 +72,7 @@ export function AddButton({ pageApi, uiActions }: { pageApi: PageApi; uiActions:
|
|||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}}
|
||||
>
|
||||
Add
|
||||
Add panel
|
||||
</EuiButton>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"id": "gridExample",
|
||||
"server": false,
|
||||
"browser": true,
|
||||
"requiredPlugins": ["developerExamples"],
|
||||
"requiredPlugins": ["developerExamples", "embeddable", "uiActions", "embeddableExamples"],
|
||||
"requiredBundles": []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -45,23 +46,37 @@ 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 }));
|
||||
|
@ -69,58 +84,31 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
// 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'}
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -180,7 +155,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
})}
|
||||
options={[
|
||||
{
|
||||
id: 'VIEW',
|
||||
id: 'view',
|
||||
label: i18n.translate('examples.gridExample.viewOption', {
|
||||
defaultMessage: 'View',
|
||||
}),
|
||||
|
@ -188,16 +163,16 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
'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>
|
||||
|
@ -245,7 +220,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<GridLayout
|
||||
accessMode={accessMode}
|
||||
accessMode={viewMode === 'view' ? 'VIEW' : 'EDIT'}
|
||||
expandedPanelId={expandedPanelId}
|
||||
layout={currentLayout}
|
||||
gridSettings={{
|
||||
|
@ -253,24 +228,27 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
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);
|
||||
};
|
||||
|
|
4884
examples/grid_example/public/logs_dashboard_panels.json
Normal file
4884
examples/grid_example/public/logs_dashboard_panels.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -9,6 +9,7 @@
|
|||
|
||||
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
|
||||
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
export const GRID_EXAMPLE_APP_ID = 'gridExample';
|
||||
const gridExampleTitle = 'Grid Example';
|
||||
|
@ -17,20 +18,28 @@ interface GridExamplePluginSetupDependencies {
|
|||
developerExamples: DeveloperExamplesSetup;
|
||||
}
|
||||
|
||||
export interface GridExamplePluginStartDependencies {
|
||||
uiActions: UiActionsStart;
|
||||
}
|
||||
|
||||
export class GridExamplePlugin
|
||||
implements Plugin<void, void, GridExamplePluginSetupDependencies, {}>
|
||||
implements
|
||||
Plugin<void, void, GridExamplePluginSetupDependencies, GridExamplePluginStartDependencies>
|
||||
{
|
||||
public setup(core: CoreSetup<{}>, { developerExamples }: GridExamplePluginSetupDependencies) {
|
||||
public setup(
|
||||
core: CoreSetup<GridExamplePluginStartDependencies>,
|
||||
{ developerExamples }: GridExamplePluginSetupDependencies
|
||||
) {
|
||||
core.application.register({
|
||||
id: GRID_EXAMPLE_APP_ID,
|
||||
title: gridExampleTitle,
|
||||
visibleIn: [],
|
||||
async mount(params: AppMountParameters) {
|
||||
const [{ renderGridExampleApp }, [coreStart]] = await Promise.all([
|
||||
const [{ renderGridExampleApp }, [coreStart, deps]] = await Promise.all([
|
||||
import('./app'),
|
||||
core.getStartServices(),
|
||||
]);
|
||||
return renderGridExampleApp(params.element, coreStart);
|
||||
return renderGridExampleApp(params.element, { coreStart, uiActions: deps.uiActions });
|
||||
},
|
||||
});
|
||||
developerExamples.register({
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
import { MockSerializedDashboardState } from './types';
|
||||
|
||||
import logsPanels from './logs_dashboard_panels.json';
|
||||
|
||||
const STATE_SESSION_STORAGE_KEY = 'kibana.examples.gridExample.state';
|
||||
|
||||
export function clearSerializedDashboardState() {
|
||||
|
@ -25,21 +27,11 @@ export function setSerializedGridLayout(state: MockSerializedDashboardState) {
|
|||
}
|
||||
|
||||
const initialState: MockSerializedDashboardState = {
|
||||
panels: {
|
||||
panel1: { id: 'panel1', gridData: { i: 'panel1', x: 0, y: 0, w: 12, h: 6, row: 0 } },
|
||||
panel2: { id: 'panel2', gridData: { i: 'panel2', x: 0, y: 6, w: 8, h: 4, row: 0 } },
|
||||
panel3: { id: 'panel3', gridData: { i: 'panel3', x: 8, y: 6, w: 12, h: 4, row: 0 } },
|
||||
panel4: { id: 'panel4', gridData: { i: 'panel4', x: 0, y: 10, w: 48, h: 4, row: 0 } },
|
||||
panel5: { id: 'panel5', gridData: { i: 'panel5', x: 12, y: 0, w: 36, h: 6, row: 0 } },
|
||||
panel6: { id: 'panel6', gridData: { i: 'panel6', x: 24, y: 6, w: 24, h: 4, row: 0 } },
|
||||
panel7: { id: 'panel7', gridData: { i: 'panel7', x: 20, y: 6, w: 4, h: 2, row: 0 } },
|
||||
panel8: { id: 'panel8', gridData: { i: 'panel8', x: 20, y: 8, w: 4, h: 2, row: 0 } },
|
||||
panel9: { id: 'panel9', gridData: { i: 'panel9', x: 0, y: 0, w: 12, h: 16, row: 1 } },
|
||||
panel10: { id: 'panel10', gridData: { i: 'panel10', x: 24, y: 0, w: 12, h: 6, row: 2 } },
|
||||
},
|
||||
panels: logsPanels,
|
||||
rows: [
|
||||
{ title: 'Large section', collapsed: false },
|
||||
{ title: 'Small section', collapsed: false },
|
||||
{ title: 'Another small section', collapsed: false },
|
||||
{ title: 'Request Sizes', collapsed: false },
|
||||
{ title: 'Visitors', collapsed: false },
|
||||
{ title: 'Response Codes', collapsed: false },
|
||||
{ title: 'Entire Flights Dashboard', collapsed: true },
|
||||
],
|
||||
};
|
||||
|
|
|
@ -15,8 +15,15 @@ export interface DashboardGridData {
|
|||
i: string;
|
||||
}
|
||||
|
||||
interface DashboardPanelState {
|
||||
type: string;
|
||||
gridData: DashboardGridData & { row?: number };
|
||||
explicitInput: Partial<any> & { id: string };
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface MockedDashboardPanelMap {
|
||||
[key: string]: { id: string; gridData: DashboardGridData & { row: number } };
|
||||
[key: string]: DashboardPanelState;
|
||||
}
|
||||
|
||||
export type MockedDashboardRowMap = Array<{ title: string; collapsed: boolean }>;
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import { PanelPackage } from '@kbn/presentation-containers';
|
||||
|
||||
import {
|
||||
MockSerializedDashboardState,
|
||||
|
@ -27,24 +31,50 @@ export const useMockDashboardApi = ({
|
|||
savedState: MockSerializedDashboardState;
|
||||
}) => {
|
||||
const mockDashboardApi = useMemo(() => {
|
||||
const panels$ = new BehaviorSubject<MockedDashboardPanelMap>(savedState.panels);
|
||||
const expandedPanelId$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
|
||||
return {
|
||||
getSerializedStateForChild: (id: string) => {
|
||||
return {
|
||||
rawState: panels$.getValue()[id].explicitInput,
|
||||
references: [],
|
||||
};
|
||||
},
|
||||
children$: new BehaviorSubject({}),
|
||||
timeRange$: new BehaviorSubject<TimeRange>({
|
||||
from: 'now-24h',
|
||||
to: 'now',
|
||||
}),
|
||||
viewMode: new BehaviorSubject('edit'),
|
||||
panels$: new BehaviorSubject<MockedDashboardPanelMap>(savedState.panels),
|
||||
panels$,
|
||||
rows$: new BehaviorSubject<MockedDashboardRowMap>(savedState.rows),
|
||||
expandedPanelId: expandedPanelId$,
|
||||
expandPanel: (id: string) => {
|
||||
if (expandedPanelId$.getValue()) {
|
||||
expandedPanelId$.next(undefined);
|
||||
} else {
|
||||
expandedPanelId$.next(id);
|
||||
}
|
||||
},
|
||||
removePanel: (id: string) => {
|
||||
const panels = { ...mockDashboardApi.panels$.getValue() };
|
||||
delete panels[id]; // the grid layout component will handle compacting, if necessary
|
||||
mockDashboardApi.panels$.next(panels);
|
||||
},
|
||||
replacePanel: (oldId: string, newId: string) => {
|
||||
replacePanel: (id: string, newPanel: PanelPackage) => {
|
||||
const currentPanels = mockDashboardApi.panels$.getValue();
|
||||
const otherPanels = { ...currentPanels };
|
||||
const oldPanel = currentPanels[oldId];
|
||||
delete otherPanels[oldId];
|
||||
otherPanels[newId] = { id: newId, gridData: { ...oldPanel.gridData, i: newId } };
|
||||
const oldPanel = currentPanels[id];
|
||||
delete otherPanels[id];
|
||||
const newId = v4();
|
||||
otherPanels[newId] = {
|
||||
...oldPanel,
|
||||
explicitInput: { ...newPanel.initialState, id: newId },
|
||||
};
|
||||
mockDashboardApi.panels$.next(otherPanels);
|
||||
},
|
||||
addNewPanel: ({ id: newId }: { id: string }) => {
|
||||
addNewPanel: async (panelPackage: PanelPackage) => {
|
||||
// we are only implementing "place at top" here, for demo purposes
|
||||
const currentPanels = mockDashboardApi.panels$.getValue();
|
||||
const otherPanels = { ...currentPanels };
|
||||
|
@ -53,17 +83,22 @@ export const useMockDashboardApi = ({
|
|||
currentPanel.gridData.y = currentPanel.gridData.y + DEFAULT_PANEL_HEIGHT;
|
||||
otherPanels[id] = currentPanel;
|
||||
}
|
||||
const newId = v4();
|
||||
mockDashboardApi.panels$.next({
|
||||
...otherPanels,
|
||||
[newId]: {
|
||||
id: newId,
|
||||
type: panelPackage.panelType,
|
||||
gridData: {
|
||||
i: newId,
|
||||
row: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: DEFAULT_PANEL_WIDTH,
|
||||
h: DEFAULT_PANEL_HEIGHT,
|
||||
i: newId,
|
||||
},
|
||||
explicitInput: {
|
||||
...panelPackage.initialState,
|
||||
id: newId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import { GridLayoutData } from '@kbn/grid-layout';
|
|||
import { MockedDashboardPanelMap, MockedDashboardRowMap } from './types';
|
||||
|
||||
export const gridLayoutToDashboardPanelMap = (
|
||||
panelState: MockedDashboardPanelMap,
|
||||
layout: GridLayoutData
|
||||
): { panels: MockedDashboardPanelMap; rows: MockedDashboardRowMap } => {
|
||||
const panels: MockedDashboardPanelMap = {};
|
||||
|
@ -19,7 +20,7 @@ export const gridLayoutToDashboardPanelMap = (
|
|||
rows.push({ title: row.title, collapsed: row.isCollapsed });
|
||||
Object.values(row.panels).forEach((panelGridData) => {
|
||||
panels[panelGridData.id] = {
|
||||
id: panelGridData.id,
|
||||
...panelState[panelGridData.id],
|
||||
gridData: {
|
||||
i: panelGridData.id,
|
||||
y: panelGridData.row,
|
||||
|
@ -49,7 +50,7 @@ export const dashboardInputToGridLayout = ({
|
|||
|
||||
Object.keys(panels).forEach((panelId) => {
|
||||
const gridData = panels[panelId].gridData;
|
||||
layout[gridData.row].panels[panelId] = {
|
||||
layout[gridData.row ?? 0].panels[panelId] = {
|
||||
id: panelId,
|
||||
row: gridData.y,
|
||||
column: gridData.x,
|
||||
|
|
|
@ -3,7 +3,13 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": ["index.ts", "public/**/*.ts", "public/**/*.tsx", "../../typings/**/*"],
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"../../typings/**/*",
|
||||
"public/**/*.json"
|
||||
],
|
||||
"exclude": ["target/**/*"],
|
||||
"kbn_references": [
|
||||
"@kbn/grid-layout",
|
||||
|
@ -11,7 +17,15 @@
|
|||
"@kbn/core",
|
||||
"@kbn/developer-examples-plugin",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/i18n",
|
||||
"@kbn/embeddable-examples-plugin",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/es-query",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/embeddable-examples-plugin",
|
||||
"@kbn/presentation-containers"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
import { css } from '@emotion/react';
|
||||
import React, { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { GridLayoutStateManager } from './types';
|
||||
|
||||
export const GridHeightSmoother = ({
|
||||
|
@ -30,6 +29,7 @@ export const GridHeightSmoother = ({
|
|||
}
|
||||
if (!interactionEvent) {
|
||||
smoothHeightRef.current.style.height = `${dimensions.height}px`;
|
||||
smoothHeightRef.current.style.userSelect = 'auto';
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ export const GridHeightSmoother = ({
|
|||
dimensions.height ?? 0,
|
||||
smoothHeightRef.current.getBoundingClientRect().height
|
||||
)}px`;
|
||||
smoothHeightRef.current.style.userSelect = 'none';
|
||||
});
|
||||
|
||||
const expandedPanelSubscription = gridLayoutStateManager.expandedPanelId$.subscribe(
|
||||
|
@ -49,19 +50,9 @@ export const GridHeightSmoother = ({
|
|||
if (!smoothHeightRef.current) return;
|
||||
|
||||
if (expandedPanelId) {
|
||||
const smoothHeightRefY =
|
||||
smoothHeightRef.current.getBoundingClientRect().y + document.documentElement.scrollTop;
|
||||
const gutterSize = parseFloat(euiThemeVars.euiSizeL);
|
||||
|
||||
// When panel is expanded, ensure the page occupies the full viewport height
|
||||
// If the parent element is a flex container (preferred approach):
|
||||
smoothHeightRef.current.style.flexBasis = `100%`;
|
||||
|
||||
// fallback in case parent is not a flex container (less reliable if shifts happen after the time we calculate smoothHeightRefY)
|
||||
smoothHeightRef.current.style.height = `calc(100vh - ${smoothHeightRefY + gutterSize}px`;
|
||||
smoothHeightRef.current.style.height = `100%`;
|
||||
smoothHeightRef.current.style.transition = 'none';
|
||||
} else {
|
||||
smoothHeightRef.current.style.flexBasis = '';
|
||||
smoothHeightRef.current.style.height = '';
|
||||
smoothHeightRef.current.style.transition = '';
|
||||
}
|
||||
|
@ -78,6 +69,8 @@ export const GridHeightSmoother = ({
|
|||
<div
|
||||
ref={smoothHeightRef}
|
||||
css={css`
|
||||
// the guttersize cannot currently change, so it's safe to set it just once
|
||||
padding: ${gridLayoutStateManager.runtimeSettings$.getValue().gutterSize};
|
||||
overflow-anchor: none;
|
||||
transition: height 500ms linear;
|
||||
`}
|
||||
|
|
|
@ -24,7 +24,10 @@ import { resolveGridRow } from './utils/resolve_grid_row';
|
|||
export interface GridLayoutProps {
|
||||
layout: GridLayoutData;
|
||||
gridSettings: GridSettings;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
renderPanelContents: (
|
||||
panelId: string,
|
||||
setDragHandles?: (refs: Array<HTMLElement | null>) => void
|
||||
) => React.ReactNode;
|
||||
onLayoutChange: (newLayout: GridLayoutData) => void;
|
||||
expandedPanelId?: string;
|
||||
accessMode?: GridAccessMode;
|
||||
|
|
|
@ -7,24 +7,88 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
import { EuiIcon, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PanelInteractionEvent } from '../types';
|
||||
import { GridLayoutStateManager, PanelInteractionEvent } from '../types';
|
||||
|
||||
export const DragHandle = ({
|
||||
interactionStart,
|
||||
}: {
|
||||
interactionStart: (
|
||||
type: PanelInteractionEvent['type'] | 'drop',
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void;
|
||||
}) => {
|
||||
export interface DragHandleApi {
|
||||
setDragHandles: (refs: Array<HTMLElement | null>) => void;
|
||||
}
|
||||
|
||||
export const DragHandle = React.forwardRef<
|
||||
DragHandleApi,
|
||||
{
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
interactionStart: (
|
||||
type: PanelInteractionEvent['type'] | 'drop',
|
||||
e: MouseEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void;
|
||||
}
|
||||
>(({ gridLayoutStateManager, interactionStart }, ref) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
|
||||
const removeEventListenersRef = useRef<(() => void) | null>(null);
|
||||
const [dragHandleCount, setDragHandleCount] = useState<number>(0);
|
||||
const dragHandleRefs = useRef<Array<HTMLElement | null>>([]);
|
||||
|
||||
/**
|
||||
* We need to memoize the `onMouseDown` callback so that we don't assign a new `onMouseDown` event handler
|
||||
* every time `setDragHandles` is called
|
||||
*/
|
||||
const onMouseDown = useCallback(
|
||||
(e: MouseEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
if (gridLayoutStateManager.accessMode$.getValue() !== 'EDIT' || e.button !== 0) {
|
||||
// ignore anything but left clicks, and ignore clicks when not in edit mode
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
interactionStart('drag', e);
|
||||
},
|
||||
[interactionStart, gridLayoutStateManager.accessMode$]
|
||||
);
|
||||
|
||||
const setDragHandles = useCallback(
|
||||
(dragHandles: Array<HTMLElement | null>) => {
|
||||
setDragHandleCount(dragHandles.length);
|
||||
dragHandleRefs.current = dragHandles;
|
||||
|
||||
for (const handle of dragHandles) {
|
||||
if (handle === null) return;
|
||||
handle.addEventListener('mousedown', onMouseDown, { passive: true });
|
||||
}
|
||||
|
||||
removeEventListenersRef.current = () => {
|
||||
for (const handle of dragHandles) {
|
||||
if (handle === null) return;
|
||||
handle.removeEventListener('mousedown', onMouseDown);
|
||||
}
|
||||
};
|
||||
},
|
||||
[onMouseDown]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// on unmount, remove all drag handle event listeners
|
||||
if (removeEventListenersRef.current) {
|
||||
removeEventListenersRef.current();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return { setDragHandles };
|
||||
},
|
||||
[setDragHandles]
|
||||
);
|
||||
|
||||
return Boolean(dragHandleCount) ? null : (
|
||||
<button
|
||||
aria-label={i18n.translate('kbnGridLayout.dragHandle.ariaLabel', {
|
||||
defaultMessage: 'Drag to move',
|
||||
|
@ -71,4 +135,4 @@ export const DragHandle = ({
|
|||
<EuiIcon type="grabOmnidirectional" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
@ -7,24 +7,27 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { forwardRef, useEffect, useMemo } from 'react';
|
||||
import React, { forwardRef, useEffect, useMemo, useState } from 'react';
|
||||
import { combineLatest, skip } from 'rxjs';
|
||||
|
||||
import { EuiPanel, euiFullHeight, useEuiOverflowScroll } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
|
||||
import { GridLayoutStateManager, PanelInteractionEvent } from '../types';
|
||||
import { getKeysInOrder } from '../utils/resolve_grid_row';
|
||||
import { DragHandle } from './drag_handle';
|
||||
import { DragHandle, DragHandleApi } from './drag_handle';
|
||||
import { ResizeHandle } from './resize_handle';
|
||||
|
||||
export interface GridPanelProps {
|
||||
panelId: string;
|
||||
rowIndex: number;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
renderPanelContents: (
|
||||
panelId: string,
|
||||
setDragHandles?: (refs: Array<HTMLElement | null>) => void
|
||||
) => React.ReactNode;
|
||||
interactionStart: (
|
||||
type: PanelInteractionEvent['type'] | 'drop',
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
e: MouseEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void;
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}
|
||||
|
@ -34,6 +37,37 @@ export const GridPanel = forwardRef<HTMLDivElement, GridPanelProps>(
|
|||
{ panelId, rowIndex, renderPanelContents, interactionStart, gridLayoutStateManager },
|
||||
panelRef
|
||||
) => {
|
||||
const [dragHandleApi, setDragHandleApi] = useState<DragHandleApi | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onDropEventHandler = (dropEvent: MouseEvent) => interactionStart('drop', dropEvent);
|
||||
/**
|
||||
* Subscription to add a singular "drop" event handler whenever an interaction starts -
|
||||
* this is handled in a subscription so that it is not lost when the component gets remounted
|
||||
* (which happens when a panel gets dragged from one grid row to another)
|
||||
*/
|
||||
const dropEventSubscription = gridLayoutStateManager.interactionEvent$.subscribe((event) => {
|
||||
if (!event || event.id !== panelId) return;
|
||||
|
||||
/**
|
||||
* By adding the "drop" event listener to the document rather than the drag/resize event handler,
|
||||
* we prevent the element from getting "stuck" in an interaction; however, we only attach this event
|
||||
* listener **when the drag/resize event starts**, and it only executes once (i.e. it removes itself
|
||||
* once it executes, so we don't have to manually remove it outside of the unmount condition)
|
||||
*/
|
||||
document.addEventListener('mouseup', onDropEventHandler, {
|
||||
once: true,
|
||||
passive: true,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
dropEventSubscription.unsubscribe();
|
||||
document.removeEventListener('mouseup', onDropEventHandler); // removes the event listener on row change
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
/** Set initial styles based on state at mount to prevent styles from "blipping" */
|
||||
const initialStyles = useMemo(() => {
|
||||
const initialPanel = gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels[panelId];
|
||||
|
@ -88,24 +122,22 @@ export const GridPanel = forwardRef<HTMLDivElement, GridPanelProps>(
|
|||
// undo any "lock to grid" styles **except** for the top left corner, which stays locked
|
||||
ref.style.gridColumnStart = `${panel.column + 1}`;
|
||||
ref.style.gridRowStart = `${panel.row + 1}`;
|
||||
ref.style.gridColumnEnd = ``;
|
||||
ref.style.gridRowEnd = ``;
|
||||
ref.style.gridColumnEnd = `auto`;
|
||||
ref.style.gridRowEnd = `auto`;
|
||||
} else {
|
||||
// if the current panel is being dragged, render it with a fixed position + size
|
||||
ref.style.position = `fixed`;
|
||||
ref.style.position = 'fixed';
|
||||
|
||||
ref.style.left = `${draggingPosition.left}px`;
|
||||
ref.style.top = `${draggingPosition.top}px`;
|
||||
ref.style.width = `${draggingPosition.right - draggingPosition.left}px`;
|
||||
ref.style.height = `${draggingPosition.bottom - draggingPosition.top}px`;
|
||||
|
||||
// undo any "lock to grid" styles
|
||||
ref.style.gridColumnStart = ``;
|
||||
ref.style.gridRowStart = ``;
|
||||
ref.style.gridColumnEnd = ``;
|
||||
ref.style.gridRowEnd = ``;
|
||||
ref.style.gridArea = `auto`; // shortcut to set all grid styles to `auto`
|
||||
}
|
||||
} else {
|
||||
ref.style.zIndex = '0';
|
||||
ref.style.zIndex = `auto`;
|
||||
|
||||
// if the panel is not being dragged and/or resized, undo any fixed position styles
|
||||
ref.style.position = '';
|
||||
|
@ -175,32 +207,27 @@ export const GridPanel = forwardRef<HTMLDivElement, GridPanelProps>(
|
|||
* Memoize panel contents to prevent unnecessary re-renders
|
||||
*/
|
||||
const panelContents = useMemo(() => {
|
||||
return renderPanelContents(panelId);
|
||||
}, [panelId, renderPanelContents]);
|
||||
if (!dragHandleApi) return <></>; // delays the rendering of the panel until after dragHandleApi is defined
|
||||
return renderPanelContents(panelId, dragHandleApi.setDragHandles);
|
||||
}, [panelId, renderPanelContents, dragHandleApi]);
|
||||
|
||||
return (
|
||||
<div ref={panelRef} css={initialStyles} className="kbnGridPanel">
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder={true}
|
||||
<div
|
||||
css={css`
|
||||
padding: 0;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
`}
|
||||
>
|
||||
<DragHandle interactionStart={interactionStart} />
|
||||
<div
|
||||
css={css`
|
||||
${euiFullHeight()}
|
||||
${useEuiOverflowScroll('y', false)}
|
||||
${useEuiOverflowScroll('x', false)}
|
||||
`}
|
||||
>
|
||||
{panelContents}
|
||||
</div>
|
||||
<DragHandle
|
||||
ref={setDragHandleApi}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
interactionStart={interactionStart}
|
||||
/>
|
||||
{panelContents}
|
||||
<ResizeHandle interactionStart={interactionStart} />
|
||||
</EuiPanel>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,12 +7,11 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { transparentize } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import React from 'react';
|
||||
import { PanelInteractionEvent } from '../types';
|
||||
|
||||
export const ResizeHandle = ({
|
||||
|
@ -20,7 +19,7 @@ export const ResizeHandle = ({
|
|||
}: {
|
||||
interactionStart: (
|
||||
type: PanelInteractionEvent['type'] | 'drop',
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
e: MouseEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void;
|
||||
}) => {
|
||||
return (
|
||||
|
@ -42,7 +41,9 @@ export const ResizeHandle = ({
|
|||
margin: -2px;
|
||||
position: absolute;
|
||||
width: ${euiThemeVars.euiSizeL};
|
||||
max-width: 100%;
|
||||
height: ${euiThemeVars.euiSizeL};
|
||||
z-index: ${euiThemeVars.euiZLevel9};
|
||||
transition: opacity 0.2s, border 0.2s;
|
||||
border-radius: 7px 0 7px 0;
|
||||
border-bottom: 2px solid ${euiThemeVars.euiColorSuccess};
|
||||
|
|
|
@ -23,7 +23,10 @@ import { GridRowHeader } from './grid_row_header';
|
|||
|
||||
export interface GridRowProps {
|
||||
rowIndex: number;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
renderPanelContents: (
|
||||
panelId: string,
|
||||
setDragHandles?: (refs: Array<HTMLElement | null>) => void
|
||||
) => React.ReactNode;
|
||||
setInteractionEvent: (interactionData?: PanelInteractionEvent) => void;
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}
|
||||
|
@ -32,19 +35,13 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
|
|||
({ rowIndex, renderPanelContents, setInteractionEvent, gridLayoutStateManager }, gridRef) => {
|
||||
const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex];
|
||||
|
||||
const [panelIds, setPanelIds] = useState<string[]>(() => getKeysInOrder(currentRow.panels));
|
||||
const [panelIds, setPanelIds] = useState<string[]>(Object.keys(currentRow.panels));
|
||||
const [panelIdsInOrder, setPanelIdsInOrder] = useState<string[]>(() =>
|
||||
getKeysInOrder(currentRow.panels)
|
||||
);
|
||||
const [rowTitle, setRowTitle] = useState<string>(currentRow.title);
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(currentRow.isCollapsed);
|
||||
|
||||
/** Syncs panel IDs in order after a change in the grid layout, such as adding, removing, or reordering panels. */
|
||||
const syncPanelIds = useCallback(() => {
|
||||
const newPanelIds = getKeysInOrder(gridLayoutStateManager.gridLayout$.value[rowIndex].panels);
|
||||
const hasOrderChanged = JSON.stringify(panelIds) !== JSON.stringify(newPanelIds);
|
||||
if (hasOrderChanged) {
|
||||
setPanelIds(newPanelIds);
|
||||
}
|
||||
}, [setPanelIds, gridLayoutStateManager.gridLayout$, rowIndex, panelIds]);
|
||||
|
||||
const getRowCount = useCallback(
|
||||
(row: GridRowData) => {
|
||||
const maxRow = Object.values(row.panels).reduce((acc, panel) => {
|
||||
|
@ -152,10 +149,6 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
|
|||
* - Title
|
||||
* - Collapsed state
|
||||
* - Panel IDs (adding/removing/replacing, but not reordering)
|
||||
*
|
||||
* Note: During dragging or resizing actions, the row should not re-render because panel positions are controlled via CSS styles for performance reasons.
|
||||
* However, once the user finishes the interaction, the order of rendered panels need to be aligned with how they are displayed in the grid for accessibility reasons (screen readers and focus management).
|
||||
* This is handled in the syncPanelIds callback.
|
||||
*/
|
||||
const rowStateSubscription = gridLayoutStateManager.gridLayout$
|
||||
.pipe(
|
||||
|
@ -163,7 +156,7 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
|
|||
return {
|
||||
title: gridLayout[rowIndex].title,
|
||||
isCollapsed: gridLayout[rowIndex].isCollapsed,
|
||||
panelIds: getKeysInOrder(gridLayout[rowIndex].panels),
|
||||
panelIds: Object.keys(gridLayout[rowIndex].panels),
|
||||
};
|
||||
}),
|
||||
pairwise()
|
||||
|
@ -180,6 +173,9 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
|
|||
)
|
||||
) {
|
||||
setPanelIds(newRowData.panelIds);
|
||||
setPanelIdsInOrder(
|
||||
getKeysInOrder(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -194,64 +190,67 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
|
|||
);
|
||||
|
||||
/**
|
||||
* Memoize panel children components to prevent unnecessary re-renders
|
||||
* Memoize panel children components (independent of their order) to prevent unnecessary re-renders
|
||||
*/
|
||||
const children = useMemo(() => {
|
||||
return panelIds.map((panelId) => (
|
||||
<GridPanel
|
||||
key={panelId}
|
||||
panelId={panelId}
|
||||
rowIndex={rowIndex}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
renderPanelContents={renderPanelContents}
|
||||
interactionStart={(type, e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const children: { [panelId: string]: React.ReactNode } = useMemo(() => {
|
||||
return panelIds.reduce(
|
||||
(prev, panelId) => ({
|
||||
...prev,
|
||||
[panelId]: (
|
||||
<GridPanel
|
||||
key={panelId}
|
||||
panelId={panelId}
|
||||
rowIndex={rowIndex}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
renderPanelContents={renderPanelContents}
|
||||
interactionStart={(type, e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Disable interactions when a panel is expanded
|
||||
const isInteractive = gridLayoutStateManager.expandedPanelId$.value === undefined;
|
||||
if (!isInteractive) return;
|
||||
// Disable interactions when a panel is expanded
|
||||
const isInteractive = gridLayoutStateManager.expandedPanelId$.value === undefined;
|
||||
if (!isInteractive) return;
|
||||
|
||||
const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
if (!panelRef) return;
|
||||
const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
if (!panelRef) return;
|
||||
|
||||
const panelRect = panelRef.getBoundingClientRect();
|
||||
if (type === 'drop') {
|
||||
setInteractionEvent(undefined);
|
||||
// Ensure the row re-renders to reflect the new panel order after a drag-and-drop interaction.
|
||||
// the order of rendered panels need to be aligned with how they are displayed in the grid for accessibility reasons (screen readers and focus management).
|
||||
syncPanelIds();
|
||||
} else {
|
||||
setInteractionEvent({
|
||||
type,
|
||||
id: panelId,
|
||||
panelDiv: panelRef,
|
||||
targetRowIndex: rowIndex,
|
||||
mouseOffsets: {
|
||||
top: e.clientY - panelRect.top,
|
||||
left: e.clientX - panelRect.left,
|
||||
right: e.clientX - panelRect.right,
|
||||
bottom: e.clientY - panelRect.bottom,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
ref={(element) => {
|
||||
if (!gridLayoutStateManager.panelRefs.current[rowIndex]) {
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex] = {};
|
||||
}
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex][panelId] = element;
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}, [
|
||||
panelIds,
|
||||
rowIndex,
|
||||
gridLayoutStateManager,
|
||||
renderPanelContents,
|
||||
setInteractionEvent,
|
||||
syncPanelIds,
|
||||
]);
|
||||
const panelRect = panelRef.getBoundingClientRect();
|
||||
if (type === 'drop') {
|
||||
setInteractionEvent(undefined);
|
||||
/**
|
||||
* Ensure the row re-renders to reflect the new panel order after a drag-and-drop interaction, since
|
||||
* the order of rendered panels need to be aligned with how they are displayed in the grid for accessibility
|
||||
* reasons (screen readers and focus management).
|
||||
*/
|
||||
setPanelIdsInOrder(
|
||||
getKeysInOrder(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels)
|
||||
);
|
||||
} else {
|
||||
setInteractionEvent({
|
||||
type,
|
||||
id: panelId,
|
||||
panelDiv: panelRef,
|
||||
targetRowIndex: rowIndex,
|
||||
mouseOffsets: {
|
||||
top: e.clientY - panelRect.top,
|
||||
left: e.clientX - panelRect.left,
|
||||
right: e.clientX - panelRect.right,
|
||||
bottom: e.clientY - panelRect.bottom,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
ref={(element) => {
|
||||
if (!gridLayoutStateManager.panelRefs.current[rowIndex]) {
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex] = {};
|
||||
}
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex][panelId] = element;
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}, [panelIds, gridLayoutStateManager, renderPanelContents, rowIndex, setInteractionEvent]);
|
||||
|
||||
return (
|
||||
<div ref={rowContainer}>
|
||||
|
@ -276,7 +275,8 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
|
|||
${initialStyles};
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
{/* render the panels **in order** for accessibility, using the memoized panel components */}
|
||||
{panelIdsInOrder.map((panelId) => children[panelId])}
|
||||
<DragPreview rowIndex={rowIndex} gridLayoutStateManager={gridLayoutStateManager} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { BehaviorSubject } from 'rxjs';
|
|||
import { ObservedSize } from 'use-resize-observer/polyfilled';
|
||||
import {
|
||||
ActivePanel,
|
||||
GridAccessMode,
|
||||
GridLayoutData,
|
||||
GridLayoutStateManager,
|
||||
PanelInteractionEvent,
|
||||
|
@ -46,6 +47,7 @@ export const gridLayoutStateManagerMock: GridLayoutStateManager = {
|
|||
runtimeSettings$,
|
||||
panelRefs: { current: [] },
|
||||
rowRefs: { current: [] },
|
||||
accessMode$: new BehaviorSubject<GridAccessMode>('EDIT'),
|
||||
interactionEvent$: new BehaviorSubject<PanelInteractionEvent | undefined>(undefined),
|
||||
activePanel$: new BehaviorSubject<ActivePanel | undefined>(undefined),
|
||||
gridDimensions$: new BehaviorSubject<ObservedSize>({ width: 600, height: 900 }),
|
||||
|
|
|
@ -60,6 +60,7 @@ export interface GridLayoutStateManager {
|
|||
gridLayout$: BehaviorSubject<GridLayoutData>;
|
||||
expandedPanelId$: BehaviorSubject<string | undefined>;
|
||||
isMobileView$: BehaviorSubject<boolean>;
|
||||
accessMode$: BehaviorSubject<GridAccessMode>;
|
||||
|
||||
gridDimensions$: BehaviorSubject<ObservedSize>;
|
||||
runtimeSettings$: BehaviorSubject<RuntimeGridSettings>;
|
||||
|
|
|
@ -13,18 +13,42 @@ import { resolveGridRow } from './utils/resolve_grid_row';
|
|||
import { GridPanelData, GridLayoutStateManager } from './types';
|
||||
import { isGridDataEqual } from './utils/equality_checks';
|
||||
|
||||
const MIN_SPEED = 50;
|
||||
const MAX_SPEED = 150;
|
||||
|
||||
const scrollOnInterval = (direction: 'up' | 'down') => {
|
||||
let count = 0;
|
||||
let currentSpeed = MIN_SPEED;
|
||||
let maxSpeed = MIN_SPEED;
|
||||
let turnAroundPoint: number | undefined;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
// calculate the speed based on how long the interval has been going to create an ease effect
|
||||
// via the parabola formula `y = a(x - h)^2 + k`
|
||||
// - the starting speed is k = 50
|
||||
// - the maximum speed is 250
|
||||
// - the rate at which the speed increases is controlled by a = 0.75
|
||||
const speed = Math.min(0.75 * count ** 2 + 50, 250);
|
||||
window.scrollBy({ top: direction === 'down' ? speed : -speed, behavior: 'smooth' });
|
||||
count++;
|
||||
}, 100);
|
||||
/**
|
||||
* Since "smooth" scrolling on an interval is jittery on Chrome, we are manually creating
|
||||
* an "ease" effect via the parabola formula `y = a(x - h)^2 + k`
|
||||
*
|
||||
* Scrolling slowly speeds up as the user drags, and it slows down again as they approach the
|
||||
* top and/or bottom of the screen.
|
||||
*/
|
||||
const nearTop = direction === 'up' && scrollY < window.innerHeight;
|
||||
const nearBottom =
|
||||
direction === 'down' &&
|
||||
window.innerHeight + window.scrollY > document.body.scrollHeight - window.innerHeight;
|
||||
if (!turnAroundPoint && (nearTop || nearBottom)) {
|
||||
// reverse the direction of the parabola
|
||||
maxSpeed = currentSpeed;
|
||||
turnAroundPoint = count;
|
||||
}
|
||||
|
||||
currentSpeed = turnAroundPoint
|
||||
? Math.max(-3 * (count - turnAroundPoint) ** 2 + maxSpeed, MIN_SPEED) // slow down fast
|
||||
: Math.min(0.1 * count ** 2 + MIN_SPEED, MAX_SPEED); // speed up slowly
|
||||
window.scrollBy({
|
||||
top: direction === 'down' ? currentSpeed : -currentSpeed,
|
||||
});
|
||||
|
||||
count++; // increase the counter to increase the time interval used in the parabola formula
|
||||
}, 60);
|
||||
return interval;
|
||||
};
|
||||
|
||||
|
@ -56,7 +80,6 @@ export const useGridLayoutEvents = ({
|
|||
stopAutoScrollIfNecessary();
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const gridRowElements = gridLayoutStateManager.rowRefs.current;
|
||||
|
@ -154,8 +177,11 @@ export const useGridLayoutEvents = ({
|
|||
// auto scroll when an event is happening close to the top or bottom of the screen
|
||||
const heightPercentage =
|
||||
100 - ((window.innerHeight - mouseTargetPixel.y) / window.innerHeight) * 100;
|
||||
const startScrollingUp = !isResize && heightPercentage < 5; // don't scroll up when resizing
|
||||
const startScrollingDown = heightPercentage > 95;
|
||||
const atTheTop = window.scrollY <= 0;
|
||||
const atTheBottom = window.innerHeight + window.scrollY >= document.body.scrollHeight;
|
||||
|
||||
const startScrollingUp = !isResize && heightPercentage < 5 && !atTheTop; // don't scroll up when resizing
|
||||
const startScrollingDown = heightPercentage > 95 && !atTheBottom;
|
||||
if (startScrollingUp || startScrollingDown) {
|
||||
if (!scrollInterval.current) {
|
||||
// only start scrolling if it's not already happening
|
||||
|
@ -202,8 +228,9 @@ export const useGridLayoutEvents = ({
|
|||
calculateUserEvent(e);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('scroll', calculateUserEvent);
|
||||
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
||||
document.addEventListener('scroll', calculateUserEvent, { passive: true });
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('scroll', calculateUserEvent);
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { BehaviorSubject, combineLatest, debounceTime } from 'rxjs';
|
||||
import useResizeObserver, { type ObservedSize } from 'use-resize-observer/polyfilled';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import {
|
||||
ActivePanel,
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
RuntimeGridSettings,
|
||||
} from './types';
|
||||
import { shouldShowMobileView } from './utils/mobile_view';
|
||||
import { resolveGridRow } from './utils/resolve_grid_row';
|
||||
|
||||
export const useGridLayoutState = ({
|
||||
layout,
|
||||
|
@ -59,7 +61,12 @@ export const useGridLayoutState = ({
|
|||
}, [accessMode, accessMode$]);
|
||||
|
||||
const gridLayoutStateManager = useMemo(() => {
|
||||
const gridLayout$ = new BehaviorSubject<GridLayoutData>(layout);
|
||||
const resolvedLayout = cloneDeep(layout);
|
||||
resolvedLayout.forEach((row, rowIndex) => {
|
||||
resolvedLayout[rowIndex] = resolveGridRow(row);
|
||||
});
|
||||
|
||||
const gridLayout$ = new BehaviorSubject<GridLayoutData>(resolvedLayout);
|
||||
const gridDimensions$ = new BehaviorSubject<ObservedSize>({ width: 0, height: 0 });
|
||||
const interactionEvent$ = new BehaviorSubject<PanelInteractionEvent | undefined>(undefined);
|
||||
const activePanel$ = new BehaviorSubject<ActivePanel | undefined>(undefined);
|
||||
|
@ -77,6 +84,7 @@ export const useGridLayoutState = ({
|
|||
panelIds$,
|
||||
gridLayout$,
|
||||
activePanel$,
|
||||
accessMode$,
|
||||
gridDimensions$,
|
||||
runtimeSettings$,
|
||||
interactionEvent$,
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/embeddable-plugin",
|
||||
"owner": [
|
||||
"@elastic/kibana-presentation"
|
||||
],
|
||||
"owner": ["@elastic/kibana-presentation"],
|
||||
"group": "platform",
|
||||
"visibility": "shared",
|
||||
"description": "Adds embeddables service to Kibana",
|
||||
|
@ -19,17 +17,8 @@
|
|||
"savedObjectsManagement",
|
||||
"contentManagement"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"savedObjectsTaggingOss",
|
||||
"usageCollection"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"savedObjects",
|
||||
"kibanaUtils",
|
||||
"presentationPanel"
|
||||
],
|
||||
"extraPublicDirs": [
|
||||
"common"
|
||||
]
|
||||
"optionalPlugins": ["savedObjectsTaggingOss", "usageCollection"],
|
||||
"requiredBundles": ["savedObjects", "kibanaUtils", "presentationPanel"],
|
||||
"extraPublicDirs": ["common"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ export const ReactEmbeddableRenderer = <
|
|||
| 'hideLoader'
|
||||
| 'hideHeader'
|
||||
| 'hideInspector'
|
||||
| 'setDragHandles'
|
||||
| 'getActions'
|
||||
>;
|
||||
hidePanelChrome?: boolean;
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import { EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import { ViewMode } from '@kbn/presentation-publishing';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { getAriaLabelForTitle } from '../presentation_panel_strings';
|
||||
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
|
||||
import { PresentationPanelTitle } from './presentation_panel_title';
|
||||
|
@ -23,6 +23,7 @@ export type PresentationPanelHeaderProps<ApiType extends DefaultPresentationPane
|
|||
hideTitle?: boolean;
|
||||
panelTitle?: string;
|
||||
panelDescription?: string;
|
||||
setDragHandle: (id: string, ref: HTMLDivElement | null) => void;
|
||||
} & Pick<PresentationPanelInternalProps, 'showBadges' | 'getActions' | 'showNotifications'>;
|
||||
|
||||
export const PresentationPanelHeader = <
|
||||
|
@ -35,6 +36,7 @@ export const PresentationPanelHeader = <
|
|||
hideTitle,
|
||||
panelTitle,
|
||||
panelDescription,
|
||||
setDragHandle,
|
||||
showBadges = true,
|
||||
showNotifications = true,
|
||||
}: PresentationPanelHeaderProps<ApiType>) => {
|
||||
|
@ -45,6 +47,14 @@ export const PresentationPanelHeader = <
|
|||
getActions
|
||||
);
|
||||
|
||||
const memoizedSetDragHandle = useCallback(
|
||||
// memoize the ref callback so that we don't call `setDragHandle` on every render
|
||||
(ref: HTMLHeadingElement | null) => {
|
||||
setDragHandle('panelHeader', ref);
|
||||
},
|
||||
[setDragHandle]
|
||||
);
|
||||
|
||||
const showPanelBar =
|
||||
(!hideTitle && panelTitle) || badgeElements.length > 0 || notificationElements.length > 0;
|
||||
|
||||
|
@ -71,7 +81,7 @@ export const PresentationPanelHeader = <
|
|||
className={headerClasses}
|
||||
data-test-subj={`embeddablePanelHeading-${(panelTitle || '').replace(/\s/g, '')}`}
|
||||
>
|
||||
<h2 data-test-subj="dashboardPanelTitle" className={titleClasses}>
|
||||
<h2 ref={memoizedSetDragHandle} data-test-subj="dashboardPanelTitle" className={titleClasses}>
|
||||
{ariaLabelElement}
|
||||
<PresentationPanelTitle
|
||||
api={api}
|
||||
|
|
|
@ -101,6 +101,7 @@ export const PresentationPanelHoverActions = ({
|
|||
api,
|
||||
index,
|
||||
getActions,
|
||||
setDragHandle,
|
||||
actionPredicate,
|
||||
children,
|
||||
className,
|
||||
|
@ -111,6 +112,7 @@ export const PresentationPanelHoverActions = ({
|
|||
index?: number;
|
||||
api: DefaultPresentationPanelApi | null;
|
||||
getActions: PresentationPanelInternalProps['getActions'];
|
||||
setDragHandle: (id: string, ref: HTMLElement | null) => void;
|
||||
actionPredicate?: (actionId: string) => boolean;
|
||||
children: ReactElement;
|
||||
className?: string;
|
||||
|
@ -124,9 +126,10 @@ export const PresentationPanelHoverActions = ({
|
|||
const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
|
||||
const [notifications, setNotifications] = useState<AnyApiAction[]>([]);
|
||||
const hoverActionsRef = useRef<HTMLDivElement | null>(null);
|
||||
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
||||
const anchorRef = useRef<HTMLDivElement | null>(null);
|
||||
const leftHoverActionsRef = useRef<HTMLDivElement | null>(null);
|
||||
const rightHoverActionsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [combineHoverActions, setCombineHoverActions] = useState<boolean>(false);
|
||||
const [borderStyles, setBorderStyles] = useState<string>(TOP_ROUNDED_CORNERS);
|
||||
|
||||
|
@ -138,14 +141,14 @@ export const PresentationPanelHoverActions = ({
|
|||
const anchorWidth = anchorRef.current.offsetWidth;
|
||||
const hoverActionsWidth =
|
||||
(rightHoverActionsRef.current?.offsetWidth ?? 0) +
|
||||
(leftHoverActionsRef.current?.offsetWidth ?? 0) +
|
||||
(dragHandleRef.current?.offsetWidth ?? 0) +
|
||||
parseInt(euiThemeVars.euiSize, 10) * 2;
|
||||
const hoverActionsHeight = rightHoverActionsRef.current?.offsetHeight ?? 0;
|
||||
|
||||
// Left align hover actions when they would get cut off by the right edge of the window
|
||||
if (anchorLeft - (hoverActionsWidth - anchorWidth) <= parseInt(euiThemeVars.euiSize, 10)) {
|
||||
hoverActionsRef.current.style.removeProperty('right');
|
||||
hoverActionsRef.current.style.setProperty('left', '0');
|
||||
dragHandleRef.current?.style.removeProperty('right');
|
||||
dragHandleRef.current?.style.setProperty('left', '0');
|
||||
} else {
|
||||
hoverActionsRef.current.style.removeProperty('left');
|
||||
hoverActionsRef.current.style.setProperty('right', '0');
|
||||
|
@ -442,19 +445,30 @@ export const PresentationPanelHoverActions = ({
|
|||
/>
|
||||
);
|
||||
|
||||
const dragHandle = (
|
||||
<EuiIcon
|
||||
type="move"
|
||||
color="text"
|
||||
className={`${viewMode === 'edit' ? 'embPanel--dragHandle' : ''}`}
|
||||
aria-label={i18n.translate('presentationPanel.dragHandle', {
|
||||
defaultMessage: 'Move panel',
|
||||
})}
|
||||
data-test-subj="embeddablePanelDragHandle"
|
||||
css={css`
|
||||
margin: ${euiThemeVars.euiSizeXS};
|
||||
`}
|
||||
/>
|
||||
const dragHandle = useMemo(
|
||||
// memoize the drag handle to avoid calling `setDragHandle` unnecessarily
|
||||
() => (
|
||||
<button
|
||||
ref={(ref) => {
|
||||
dragHandleRef.current = ref;
|
||||
setDragHandle('hoverActions', ref);
|
||||
}}
|
||||
>
|
||||
<EuiIcon
|
||||
type="move"
|
||||
color="text"
|
||||
className={`embPanel--dragHandle`}
|
||||
aria-label={i18n.translate('presentationPanel.dragHandle', {
|
||||
defaultMessage: 'Move panel',
|
||||
})}
|
||||
data-test-subj="embeddablePanelDragHandle"
|
||||
css={css`
|
||||
margin: ${euiThemeVars.euiSizeXS};
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
),
|
||||
[setDragHandle]
|
||||
);
|
||||
|
||||
const hasHoverActions = quickActionElements.length || contextMenuPanels.lastIndexOf.length;
|
||||
|
@ -535,7 +549,6 @@ export const PresentationPanelHoverActions = ({
|
|||
>
|
||||
{viewMode === 'edit' && !combineHoverActions ? (
|
||||
<div
|
||||
ref={leftHoverActionsRef}
|
||||
data-test-subj="embPanel__hoverActions__left"
|
||||
className={classNames(
|
||||
'embPanel__hoverActions',
|
||||
|
|
|
@ -15,9 +15,9 @@ import {
|
|||
useBatchedOptionalPublishingSubjects,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import classNames from 'classnames';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { PresentationPanelHoverActions } from './panel_header/presentation_panel_hover_actions';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { PresentationPanelHeader } from './panel_header/presentation_panel_header';
|
||||
import { PresentationPanelHoverActions } from './panel_header/presentation_panel_hover_actions';
|
||||
import { PresentationPanelError } from './presentation_panel_error';
|
||||
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from './types';
|
||||
|
||||
|
@ -37,10 +37,14 @@ export const PresentationPanelInternal = <
|
|||
|
||||
Component,
|
||||
componentProps,
|
||||
|
||||
setDragHandles,
|
||||
}: PresentationPanelInternalProps<ApiType, ComponentPropsType>) => {
|
||||
const [api, setApi] = useState<ApiType | null>(null);
|
||||
const headerId = useMemo(() => htmlIdGenerator()(), []);
|
||||
|
||||
const dragHandles = useRef<{ [dragHandleKey: string]: HTMLElement | null }>({});
|
||||
|
||||
const viewModeSubject = (() => {
|
||||
if (apiPublishesViewMode(api)) return api.viewMode;
|
||||
if (apiHasParentApi(api) && apiPublishesViewMode(api.parentApi)) return api.parentApi.viewMode;
|
||||
|
@ -90,9 +94,26 @@ export const PresentationPanelInternal = <
|
|||
return attrs;
|
||||
}, [dataLoading, blockingError]);
|
||||
|
||||
const setDragHandle = useCallback(
|
||||
(id: string, ref: HTMLElement | null) => {
|
||||
dragHandles.current[id] = ref;
|
||||
setDragHandles?.(Object.values(dragHandles.current));
|
||||
},
|
||||
[setDragHandles]
|
||||
);
|
||||
|
||||
return (
|
||||
<PresentationPanelHoverActions
|
||||
{...{ index, api, getActions, actionPredicate, viewMode, showNotifications, showBorder }}
|
||||
{...{
|
||||
index,
|
||||
api,
|
||||
getActions,
|
||||
actionPredicate,
|
||||
viewMode,
|
||||
showNotifications,
|
||||
showBorder,
|
||||
}}
|
||||
setDragHandle={setDragHandle}
|
||||
>
|
||||
<EuiPanel
|
||||
role="figure"
|
||||
|
@ -108,6 +129,7 @@ export const PresentationPanelInternal = <
|
|||
{!hideHeader && api && (
|
||||
<PresentationPanelHeader
|
||||
api={api}
|
||||
setDragHandle={setDragHandle}
|
||||
headerId={headerId}
|
||||
viewMode={viewMode}
|
||||
hideTitle={hideTitle}
|
||||
|
|
|
@ -58,6 +58,13 @@ export interface PresentationPanelInternalProps<
|
|||
* "title" when the panel has no title, i.e. "Panel {index}".
|
||||
*/
|
||||
index?: number;
|
||||
|
||||
/**
|
||||
* Set the drag handlers to be used by kbn-grid-layout
|
||||
* Note: If we make kbn-grid-layout responsible for **all** panel placement
|
||||
* logic, then this could be removed.
|
||||
*/
|
||||
setDragHandles?: (refs: Array<HTMLElement | null>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue