Skip to content

Commit

Permalink
[Dashboard][Collapsable Panels] Swap react-grid-layout for `kbn-gri…
Browse files Browse the repository at this point in the history
…d-layout` (elastic#205341)

Closes elastic#190446

## Summary

This PR swaps out `react-grid-layout` for the new internal
`kbn-grid-layout` in the Dashboard plugin. This is the first major step
in making collapsible sections possible in Dashboard.

- **`react-grid-layout` (before)**:


https://github.com/user-attachments/assets/ca6ec059-7f4a-43fb-890e-7b72b781e50b

- **`kbn-grid-layout` (after)**:


https://github.com/user-attachments/assets/3d3de1f3-1afc-4e6b-93d6-9cc31a46e2cf

### Notable Improvements

- Better handling of resizing panels near the bottom of the screen
   
  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Jan-09-2025
09-59-00](https://github.com/user-attachments/assets/75854b76-3ad7-4f06-9745-b03bde15f87a)
| ![Jan-09-2025
09-26-24](https://github.com/user-attachments/assets/f0fbc0bf-9208-4866-b7eb-988c7abc3e50)
|


- Auto-scroll when dragging / resizing panels near the top and bottom of
the screen, making it much easier to move panels around by larger
distances

  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Jan-09-2025
10-01-30](https://github.com/user-attachments/assets/e3457e5e-3647-4024-b6e6-c594d6d3e1d7)
| ![Jan-09-2025
09-25-35](https://github.com/user-attachments/assets/3252bdec-2bbc-4793-b089-346866d4589b)
|

- More reliable panel positioning due to the use of CSS grid rather than
absolute positioning via pixels

  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Screenshot 2025-01-09 at 9 32
52 AM](https://github.com/user-attachments/assets/06bd31a4-0a9f-4561-84c3-4cd96ba297b0)
| ![Screenshot 2025-01-09 at 9 35
14 AM](https://github.com/user-attachments/assets/573dab98-3fb9-4ef6-9f37-c4cf4d03ce52)
|

- Better performance when dragging and resizing (see
elastic#204134 for a more thorough
explanation) and a smaller bundle size than `react-grid-layout`

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

This PR contains a significant change to the Dashboard layout engine,
which means that it carries a decent amount of risk for introducing new,
uncaught bugs with dragging / resizing panels and collision resolution.
That being said, `kbn-grid-layout` has been built **iteratively** with
plenty of testing along the way to reduce this risk.

## Release note
Improves Dashboard layout engine by switching to the internally
developed `kbn-grid-layout`.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 14, 2025
1 parent 2c9e55d commit 6865715
Show file tree
Hide file tree
Showing 26 changed files with 514 additions and 610 deletions.
2 changes: 1 addition & 1 deletion examples/grid_example/public/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export const GridExample = ({
<EuiPageTemplate.Section
color="subdued"
contentProps={{
css: { flexGrow: 1 },
css: { flexGrow: 1, display: 'flex', flexDirection: 'column' },
}}
>
<EuiCallOut
Expand Down
59 changes: 34 additions & 25 deletions packages/kbn-grid-layout/grid/grid_height_smoother.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { css } from '@emotion/react';
import React, { PropsWithChildren, useEffect, useRef } from 'react';
import { combineLatest } from 'rxjs';
import { combineLatest, distinctUntilChanged, map } from 'rxjs';
import { GridLayoutStateManager } from './types';

export const GridHeightSmoother = ({
Expand All @@ -18,61 +18,70 @@ export const GridHeightSmoother = ({
}: PropsWithChildren<{ gridLayoutStateManager: GridLayoutStateManager }>) => {
// set the parent div size directly to smooth out height changes.
const smoothHeightRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
/**
* When the user is interacting with an element, the page can grow, but it cannot
* shrink. This is to stop a behaviour where the page would scroll up automatically
* making the panel shrink or grow unpredictably.
*/
const interactionStyleSubscription = combineLatest([
gridLayoutStateManager.gridDimensions$,
gridLayoutStateManager.interactionEvent$,
]).subscribe(([dimensions, interactionEvent]) => {
if (!smoothHeightRef.current) return;
if (gridLayoutStateManager.expandedPanelId$.getValue()) {
return;
}
if (!smoothHeightRef.current || gridLayoutStateManager.expandedPanelId$.getValue()) return;

if (!interactionEvent) {
smoothHeightRef.current.style.height = `${dimensions.height}px`;
smoothHeightRef.current.style.userSelect = 'auto';
return;
}

/**
* When the user is interacting with an element, the page can grow, but it cannot
* shrink. This is to stop a behaviour where the page would scroll up automatically
* making the panel shrink or grow unpredictably.
*/
smoothHeightRef.current.style.height = `${Math.max(
dimensions.height ?? 0,
smoothHeightRef.current.getBoundingClientRect().height
)}px`;
smoothHeightRef.current.style.userSelect = 'none';
});

const expandedPanelSubscription = gridLayoutStateManager.expandedPanelId$.subscribe(
(expandedPanelId) => {
if (!smoothHeightRef.current) return;
/**
* This subscription sets global CSS variables that can be used by all components contained within
* this wrapper; note that this is **currently** only used for the gutter size, but things like column
* count could be added here once we add the ability to change these values
*/
const globalCssVariableSubscription = gridLayoutStateManager.runtimeSettings$
.pipe(
map(({ gutterSize }) => gutterSize),
distinctUntilChanged()
)
.subscribe((gutterSize) => {
smoothHeightRef.current?.style.setProperty('--kbnGridGutterSize', `${gutterSize}`);
});

if (expandedPanelId) {
smoothHeightRef.current.style.height = `100%`;
smoothHeightRef.current.style.transition = 'none';
} else {
smoothHeightRef.current.style.height = '';
smoothHeightRef.current.style.transition = '';
}
}
);
return () => {
interactionStyleSubscription.unsubscribe();
expandedPanelSubscription.unsubscribe();
globalCssVariableSubscription.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div
ref={smoothHeightRef}
className={'kbnGridWrapper'}
css={css`
// the guttersize cannot currently change, so it's safe to set it just once
padding: ${gridLayoutStateManager.runtimeSettings$.getValue().gutterSize};
margin: calc(var(--kbnGridGutterSize) * 1px);
overflow-anchor: none;
transition: height 500ms linear;
&:has(.kbnGridPanel--expanded) {
height: 100% !important;
position: relative;
transition: none;
// switch to padding so that the panel does not extend the height of the parent
margin: 0px;
padding: calc(var(--kbnGridGutterSize) * 1px);
}
`}
>
{children}
Expand Down
94 changes: 84 additions & 10 deletions packages/kbn-grid-layout/grid/grid_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
*/

import { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import classNames from 'classnames';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { combineLatest, distinctUntilChanged, filter, map, pairwise, skip } from 'rxjs';

import { css } from '@emotion/react';

import { GridHeightSmoother } from './grid_height_smoother';
import { GridRow } from './grid_row';
import { GridAccessMode, GridLayoutData, GridSettings } from './types';
Expand Down Expand Up @@ -48,6 +48,7 @@ export const GridLayout = ({
accessMode,
});
useGridLayoutEvents({ gridLayoutStateManager });
const layoutRef = useRef<HTMLDivElement | null>(null);

const [rowCount, setRowCount] = useState<number>(
gridLayoutStateManager.gridLayout$.getValue().length
Expand Down Expand Up @@ -89,6 +90,9 @@ export const GridLayout = ({
setRowCount(newRowCount);
});

/**
* This subscription calls the passed `onLayoutChange` callback when the layout changes
*/
const onLayoutChangeSubscription = combineLatest([
gridLayoutStateManager.gridLayout$,
gridLayoutStateManager.interactionEvent$,
Expand All @@ -106,9 +110,33 @@ export const GridLayout = ({
}
});

/**
* This subscription adds and/or removes the necessary class names related to styling for
* mobile view and a static (non-interactable) grid layout
*/
const gridLayoutClassSubscription = combineLatest([
gridLayoutStateManager.accessMode$,
gridLayoutStateManager.isMobileView$,
]).subscribe(([currentAccessMode, isMobileView]) => {
if (!layoutRef) return;

if (isMobileView) {
layoutRef.current?.classList.add('kbnGrid--mobileView');
} else {
layoutRef.current?.classList.remove('kbnGrid--mobileView');
}

if (currentAccessMode === 'VIEW') {
layoutRef.current?.classList.add('kbnGrid--static');
} else {
layoutRef.current?.classList.remove('kbnGrid--static');
}
});

return () => {
rowCountSubscription.unsubscribe();
onLayoutChangeSubscription.unsubscribe();
gridLayoutClassSubscription.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand Down Expand Up @@ -138,21 +166,20 @@ export const GridLayout = ({
});
}, [rowCount, gridLayoutStateManager, renderPanelContents]);

const gridClassNames = classNames('kbnGrid', {
'kbnGrid--static': expandedPanelId || accessMode === 'VIEW',
'kbnGrid--hasExpandedPanel': Boolean(expandedPanelId),
});

return (
<GridHeightSmoother gridLayoutStateManager={gridLayoutStateManager}>
<div
ref={(divElement) => {
layoutRef.current = divElement;
setDimensionsRef(divElement);
}}
className={gridClassNames}
className="kbnGrid"
css={css`
&.kbnGrid--hasExpandedPanel {
height: 100%;
&:has(.kbnGridPanel--expanded) {
${expandedPanelStyles}
}
&.kbnGrid--mobileView {
${singleColumnStyles}
}
`}
>
Expand All @@ -161,3 +188,50 @@ export const GridLayout = ({
</GridHeightSmoother>
);
};

const singleColumnStyles = css`
.kbnGridRow {
grid-template-columns: 100%;
grid-template-rows: auto;
grid-auto-flow: row;
grid-auto-rows: auto;
}
.kbnGridPanel {
grid-area: unset !important;
}
`;

const expandedPanelStyles = css`
height: 100%;
& .kbnGridRowContainer:has(.kbnGridPanel--expanded) {
// targets the grid row container that contains the expanded panel
.kbnGridRowHeader {
height: 0px; // used instead of 'display: none' due to a11y concerns
}
.kbnGridRow {
display: block !important; // overwrite grid display
height: 100%;
.kbnGridPanel {
&.kbnGridPanel--expanded {
height: 100% !important;
}
&:not(.kbnGridPanel--expanded) {
// hide the non-expanded panels
position: absolute;
top: -9999px;
left: -9999px;
visibility: hidden; // remove hidden panels and their contents from tab order for a11y
}
}
}
}
& .kbnGridRowContainer:not(:has(.kbnGridPanel--expanded)) {
// targets the grid row containers that **do not** contain the expanded panel
position: absolute;
top: -9999px;
left: -9999px;
}
`;
3 changes: 2 additions & 1 deletion packages/kbn-grid-layout/grid/grid_panel/drag_handle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ export const DragHandle = React.forwardRef<
&:active {
cursor: grabbing;
}
.kbnGrid--static & {
.kbnGrid--static &,
.kbnGridPanel--expanded & {
display: none;
}
`}
Expand Down
Loading

0 comments on commit 6865715

Please sign in to comment.