mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Dashboard][Collapsable Panels] Responsive layout (#200771)
## Summary Fixes https://github.com/elastic/kibana/issues/197714 ### Key Features #### Responsiveness 1. Adds a responsive view controlled by the `accessMode` prop. 2. For the responsive version (in the `VIEW` mode), panels retain height and are arranged based on screen order (left-to-right, top-to-bottom). 3. Interactivity (drag/resize) is disabled in `view` mode. <img width="514" alt="Screenshot 2024-11-25 at 17 34 56" src="https://github.com/user-attachments/assets/6a5a97aa-de9b-495a-b1de-301bc935a5ab"> #### Maximization 1. Supports expanded panel view using the `expandedPanelId` prop. 2. Interactivity (drag/resize) is disabled when a panel is expanded. <img width="1254" alt="Screenshot 2024-11-25 at 17 35 05" src="https://github.com/user-attachments/assets/c83014f6-18ad-435b-a59d-1d3ba3f80d84"> #### Considerations 1. CSS elements naming convention: Main component uses `kbnGrid` class, with modifiers like `kbnGrid--nonInteractive`. For the drag handle of `GridPanel` I used `kbnGridPanel__dragHandle` classname. 2. Classes vs. Inline Styles: I opted for using `kbnGrid--nonInteractive` instead of adding one more subscription to `GridPanel` to modify the styles inline. It's the first time in this package that I used classes instead of inline styles for no-initial styles setting. 3. Naming Convention: I opted for using the `expanded` word to describe an expanded panel. Another one could be `maximized` as it's more used in UI, but less in the legacy code. 4. Interactivity (drag/resize) is disabled in responsive mode but we could consider to limit this to small viewports only (<768px). --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
650c5ca212
commit
d92aa91c3a
10 changed files with 315 additions and 39 deletions
|
@ -23,10 +23,12 @@ import {
|
|||
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 } from '@kbn/grid-layout';
|
||||
import { GridLayout, GridLayoutData, GridAccessMode } from '@kbn/grid-layout';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { getPanelId } from './get_panel_id';
|
||||
|
@ -46,6 +48,8 @@ const DASHBOARD_GRID_COLUMN_COUNT = 48;
|
|||
export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
||||
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)
|
||||
);
|
||||
|
@ -72,6 +76,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
<div style={{ padding: 8 }}>{id}</div>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
setExpandedPanelId(undefined);
|
||||
mockDashboardApi.removePanel(id);
|
||||
}}
|
||||
>
|
||||
|
@ -81,6 +86,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
</EuiButtonEmpty>
|
||||
<EuiButtonEmpty
|
||||
onClick={async () => {
|
||||
setExpandedPanelId(undefined);
|
||||
const newPanelId = await getPanelId({
|
||||
coreStart,
|
||||
suggestion: id,
|
||||
|
@ -92,10 +98,25 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
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 },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
[coreStart, mockDashboardApi]
|
||||
[coreStart, mockDashboardApi, setExpandedPanelId, expandedPanelId]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -107,7 +128,12 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
defaultMessage: 'Grid Layout Example',
|
||||
})}
|
||||
/>
|
||||
<EuiPageTemplate.Section color="subdued">
|
||||
<EuiPageTemplate.Section
|
||||
color="subdued"
|
||||
contentProps={{
|
||||
css: { display: 'flex', flexFlow: 'column nowrap', flexGrow: 1 },
|
||||
}}
|
||||
>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('examples.gridExample.sessionStorageCallout', {
|
||||
defaultMessage:
|
||||
|
@ -132,6 +158,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={async () => {
|
||||
setExpandedPanelId(undefined);
|
||||
const panelId = await getPanelId({
|
||||
coreStart,
|
||||
suggestion: uuidv4(),
|
||||
|
@ -146,6 +173,34 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('examples.gridExample.layoutOptionsLegend', {
|
||||
defaultMessage: 'Layout options',
|
||||
})}
|
||||
options={[
|
||||
{
|
||||
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',
|
||||
label: i18n.translate('examples.gridExample.editOption', {
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
toolTipContent: 'The layout does not adjust when the window is resized.',
|
||||
},
|
||||
]}
|
||||
idSelected={accessMode}
|
||||
onChange={(id) => {
|
||||
setAccessMode(id as GridAccessMode);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{hasUnsavedChanges && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="warning">
|
||||
|
@ -190,6 +245,8 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<GridLayout
|
||||
accessMode={accessMode}
|
||||
expandedPanelId={expandedPanelId}
|
||||
layout={currentLayout}
|
||||
gridSettings={{
|
||||
gutterSize: DASHBOARD_MARGIN_SIZE,
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
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 = ({
|
||||
|
@ -19,11 +20,14 @@ export const GridHeightSmoother = ({
|
|||
// set the parent div size directly to smooth out height changes.
|
||||
const smoothHeightRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
const subscription = combineLatest([
|
||||
const interactionStyleSubscription = combineLatest([
|
||||
gridLayoutStateManager.gridDimensions$,
|
||||
gridLayoutStateManager.interactionEvent$,
|
||||
]).subscribe(([dimensions, interactionEvent]) => {
|
||||
if (!smoothHeightRef.current) return;
|
||||
if (gridLayoutStateManager.expandedPanelId$.getValue()) {
|
||||
return;
|
||||
}
|
||||
if (!interactionEvent) {
|
||||
smoothHeightRef.current.style.height = `${dimensions.height}px`;
|
||||
return;
|
||||
|
@ -39,7 +43,34 @@ export const GridHeightSmoother = ({
|
|||
smoothHeightRef.current.getBoundingClientRect().height
|
||||
)}px`;
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
|
||||
const expandedPanelSubscription = gridLayoutStateManager.expandedPanelId$.subscribe(
|
||||
(expandedPanelId) => {
|
||||
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.transition = 'none';
|
||||
} else {
|
||||
smoothHeightRef.current.style.flexBasis = '';
|
||||
smoothHeightRef.current.style.height = '';
|
||||
smoothHeightRef.current.style.transition = '';
|
||||
}
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
interactionStyleSubscription.unsubscribe();
|
||||
expandedPanelSubscription.unsubscribe();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -9,11 +9,13 @@
|
|||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
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 { GridLayoutData, GridSettings } from './types';
|
||||
import { GridAccessMode, GridLayoutData, GridSettings } from './types';
|
||||
import { useGridLayoutEvents } from './use_grid_layout_events';
|
||||
import { useGridLayoutState } from './use_grid_layout_state';
|
||||
import { isLayoutEqual } from './utils/equality_checks';
|
||||
|
@ -24,6 +26,8 @@ interface GridLayoutProps {
|
|||
gridSettings: GridSettings;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
onLayoutChange: (newLayout: GridLayoutData) => void;
|
||||
expandedPanelId?: string;
|
||||
accessMode?: GridAccessMode;
|
||||
}
|
||||
|
||||
export const GridLayout = ({
|
||||
|
@ -31,10 +35,14 @@ export const GridLayout = ({
|
|||
gridSettings,
|
||||
renderPanelContents,
|
||||
onLayoutChange,
|
||||
expandedPanelId,
|
||||
accessMode = 'EDIT',
|
||||
}: GridLayoutProps) => {
|
||||
const { gridLayoutStateManager, setDimensionsRef } = useGridLayoutState({
|
||||
layout,
|
||||
gridSettings,
|
||||
expandedPanelId,
|
||||
accessMode,
|
||||
});
|
||||
useGridLayoutEvents({ gridLayoutStateManager });
|
||||
|
||||
|
@ -132,12 +140,23 @@ 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) => {
|
||||
setDimensionsRef(divElement);
|
||||
}}
|
||||
className={gridClassNames}
|
||||
css={css`
|
||||
&.kbnGrid--hasExpandedPanel {
|
||||
height: 100%;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -22,6 +22,7 @@ import { css } from '@emotion/react';
|
|||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
|
||||
import { GridLayoutStateManager, PanelInteractionEvent } from './types';
|
||||
import { getKeysInOrder } from './utils/resolve_grid_row';
|
||||
|
||||
export const GridPanel = forwardRef<
|
||||
HTMLDivElement,
|
||||
|
@ -50,13 +51,21 @@ export const GridPanel = forwardRef<
|
|||
grid-column-end: ${initialPanel.column + 1 + initialPanel.width};
|
||||
grid-row-start: ${initialPanel.row + 1};
|
||||
grid-row-end: ${initialPanel.row + 1 + initialPanel.height};
|
||||
&.kbnGridPanel--isExpanded {
|
||||
transform: translate(9999px, 9999px);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
}, [gridLayoutStateManager, rowIndex, panelId]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
/** Update the styles of the panel via a subscription to prevent re-renders */
|
||||
const styleSubscription = combineLatest([
|
||||
const activePanelStyleSubscription = combineLatest([
|
||||
gridLayoutStateManager.activePanel$,
|
||||
gridLayoutStateManager.gridLayout$,
|
||||
gridLayoutStateManager.runtimeSettings$,
|
||||
|
@ -68,6 +77,7 @@ export const GridPanel = forwardRef<
|
|||
if (!ref || !panel) return;
|
||||
|
||||
const currentInteractionEvent = gridLayoutStateManager.interactionEvent$.getValue();
|
||||
|
||||
if (panelId === activePanel?.id) {
|
||||
// if the current panel is active, give it fixed positioning depending on the interaction event
|
||||
const { position: draggingPosition } = activePanel;
|
||||
|
@ -91,7 +101,7 @@ export const GridPanel = forwardRef<
|
|||
ref.style.gridRowEnd = ``;
|
||||
} 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`;
|
||||
|
@ -121,8 +131,50 @@ export const GridPanel = forwardRef<
|
|||
}
|
||||
});
|
||||
|
||||
const expandedPanelStyleSubscription = gridLayoutStateManager.expandedPanelId$
|
||||
.pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
|
||||
.subscribe((expandedPanelId) => {
|
||||
const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
const gridLayout = gridLayoutStateManager.gridLayout$.getValue();
|
||||
const panel = gridLayout[rowIndex].panels[panelId];
|
||||
if (!ref || !panel) return;
|
||||
|
||||
if (expandedPanelId && expandedPanelId === panelId) {
|
||||
ref.classList.add('kbnGridPanel--isExpanded');
|
||||
} else {
|
||||
ref.classList.remove('kbnGridPanel--isExpanded');
|
||||
}
|
||||
});
|
||||
|
||||
const mobileViewStyleSubscription = gridLayoutStateManager.isMobileView$
|
||||
.pipe(skip(1))
|
||||
.subscribe((isMobileView) => {
|
||||
if (!isMobileView) {
|
||||
return;
|
||||
}
|
||||
const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
const gridLayout = gridLayoutStateManager.gridLayout$.getValue();
|
||||
const allPanels = gridLayout[rowIndex].panels;
|
||||
const panel = allPanels[panelId];
|
||||
if (!ref || !panel) return;
|
||||
|
||||
const sortedKeys = getKeysInOrder(gridLayout[rowIndex]);
|
||||
const currentPanelPosition = sortedKeys.indexOf(panelId);
|
||||
const sortedKeysBefore = sortedKeys.slice(0, currentPanelPosition);
|
||||
const responsiveGridRowStart = sortedKeysBefore.reduce(
|
||||
(acc, key) => acc + allPanels[key].height,
|
||||
1
|
||||
);
|
||||
ref.style.gridColumnStart = `1`;
|
||||
ref.style.gridColumnEnd = `-1`;
|
||||
ref.style.gridRowStart = `${responsiveGridRowStart}`;
|
||||
ref.style.gridRowEnd = `${responsiveGridRowStart + panel.height}`;
|
||||
});
|
||||
|
||||
return () => {
|
||||
styleSubscription.unsubscribe();
|
||||
expandedPanelStyleSubscription.unsubscribe();
|
||||
mobileViewStyleSubscription.unsubscribe();
|
||||
activePanelStyleSubscription.unsubscribe();
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@ -150,7 +202,7 @@ export const GridPanel = forwardRef<
|
|||
>
|
||||
{/* drag handle */}
|
||||
<div
|
||||
className="dragHandle"
|
||||
className="kbnGridPanel__dragHandle"
|
||||
css={css`
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
|
@ -174,6 +226,10 @@ export const GridPanel = forwardRef<
|
|||
cursor: grabbing;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.kbnGrid--static & {
|
||||
opacity: 0 !important;
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
onMouseDown={(e) => interactionStart('drag', e)}
|
||||
onMouseUp={(e) => interactionStart('drop', e)}
|
||||
|
@ -182,7 +238,7 @@ export const GridPanel = forwardRef<
|
|||
</div>
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
className="resizeHandle"
|
||||
className="kbnGridPanel__resizeHandle"
|
||||
onMouseDown={(e) => interactionStart('resize', e)}
|
||||
onMouseUp={(e) => interactionStart('drop', e)}
|
||||
css={css`
|
||||
|
@ -202,6 +258,10 @@ export const GridPanel = forwardRef<
|
|||
background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
|
||||
cursor: se-resize;
|
||||
}
|
||||
.kbnGrid--static & {
|
||||
opacity: 0 !important;
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { combineLatest, map, pairwise, skip } from 'rxjs';
|
||||
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiSpacer, EuiTitle, transparentize } from '@elastic/eui';
|
||||
|
@ -54,6 +54,7 @@ export const GridRow = forwardRef<
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[rowIndex]
|
||||
);
|
||||
const rowContainer = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
/** Set initial styles based on state at mount to prevent styles from "blipping" */
|
||||
const initialStyles = useMemo(() => {
|
||||
|
@ -74,7 +75,7 @@ export const GridRow = forwardRef<
|
|||
useEffect(
|
||||
() => {
|
||||
/** Update the styles of the grid row via a subscription to prevent re-renders */
|
||||
const styleSubscription = combineLatest([
|
||||
const interactionStyleSubscription = combineLatest([
|
||||
gridLayoutStateManager.interactionEvent$,
|
||||
gridLayoutStateManager.gridLayout$,
|
||||
gridLayoutStateManager.runtimeSettings$,
|
||||
|
@ -115,6 +116,36 @@ export const GridRow = forwardRef<
|
|||
}
|
||||
});
|
||||
|
||||
const expandedPanelStyleSubscription = gridLayoutStateManager.expandedPanelId$
|
||||
.pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
|
||||
.subscribe((expandedPanelId) => {
|
||||
const rowContainerRef = rowContainer.current;
|
||||
if (!rowContainerRef) return;
|
||||
|
||||
if (expandedPanelId) {
|
||||
// If any panel is expanded, move all rows with their panels out of the viewport.
|
||||
// The expanded panel is repositioned to its original location in the GridPanel component
|
||||
// and stretched to fill the viewport.
|
||||
|
||||
rowContainerRef.style.transform = 'translate(-9999px, -9999px)';
|
||||
|
||||
const panelsIds = Object.keys(
|
||||
gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels
|
||||
);
|
||||
const includesExpandedPanel = panelsIds.includes(expandedPanelId);
|
||||
if (includesExpandedPanel) {
|
||||
// Stretch the row with the expanded panel to occupy the entire remaining viewport
|
||||
rowContainerRef.style.height = '100%';
|
||||
} else {
|
||||
// Hide the row if it does not contain the expanded panel
|
||||
rowContainerRef.style.height = '0';
|
||||
}
|
||||
} else {
|
||||
rowContainerRef.style.transform = ``;
|
||||
rowContainerRef.style.height = ``;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The things that should trigger a re-render are title, collapsed state, and panel ids - panel positions
|
||||
* are being controlled via CSS styles, so they do not need to trigger a re-render. This subscription ensures
|
||||
|
@ -147,8 +178,9 @@ export const GridRow = forwardRef<
|
|||
});
|
||||
|
||||
return () => {
|
||||
styleSubscription.unsubscribe();
|
||||
interactionStyleSubscription.unsubscribe();
|
||||
rowStateSubscription.unsubscribe();
|
||||
expandedPanelStyleSubscription.unsubscribe();
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@ -169,6 +201,11 @@ export const GridRow = forwardRef<
|
|||
interactionStart={(type, e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 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;
|
||||
|
||||
|
@ -201,25 +238,13 @@ export const GridRow = forwardRef<
|
|||
}, [panelIds, rowIndex, gridLayoutStateManager, renderPanelContents, setInteractionEvent]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={rowContainer}>
|
||||
{rowIndex !== 0 && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
aria-label={i18n.translate('kbnGridLayout.row.toggleCollapse', {
|
||||
defaultMessage: 'Toggle collapse',
|
||||
})}
|
||||
iconType={isCollapsed ? 'arrowRight' : 'arrowDown'}
|
||||
onClick={toggleIsCollapsed}
|
||||
/>
|
||||
<EuiTitle size="xs">
|
||||
<h2>{rowTitle}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
<GridRowHeader
|
||||
isCollapsed={isCollapsed}
|
||||
toggleIsCollapsed={toggleIsCollapsed}
|
||||
rowTitle={rowTitle}
|
||||
/>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<div
|
||||
|
@ -235,7 +260,37 @@ export const GridRow = forwardRef<
|
|||
<DragPreview rowIndex={rowIndex} gridLayoutStateManager={gridLayoutStateManager} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const GridRowHeader = ({
|
||||
isCollapsed,
|
||||
toggleIsCollapsed,
|
||||
rowTitle,
|
||||
}: {
|
||||
isCollapsed: boolean;
|
||||
toggleIsCollapsed: () => void;
|
||||
rowTitle?: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
aria-label={i18n.translate('kbnGridLayout.row.toggleCollapse', {
|
||||
defaultMessage: 'Toggle collapse',
|
||||
})}
|
||||
iconType={isCollapsed ? 'arrowRight' : 'arrowDown'}
|
||||
onClick={toggleIsCollapsed}
|
||||
/>
|
||||
<EuiTitle size="xs">
|
||||
<h2>{rowTitle}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -58,6 +58,8 @@ export interface ActivePanel {
|
|||
|
||||
export interface GridLayoutStateManager {
|
||||
gridLayout$: BehaviorSubject<GridLayoutData>;
|
||||
expandedPanelId$: BehaviorSubject<string | undefined>;
|
||||
isMobileView$: BehaviorSubject<boolean>;
|
||||
|
||||
gridDimensions$: BehaviorSubject<ObservedSize>;
|
||||
runtimeSettings$: BehaviorSubject<RuntimeGridSettings>;
|
||||
|
@ -117,3 +119,5 @@ export interface PanelPlacementSettings {
|
|||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export type GridAccessMode = 'VIEW' | 'EDIT';
|
||||
|
|
|
@ -8,25 +8,30 @@
|
|||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { BehaviorSubject, debounceTime } from 'rxjs';
|
||||
|
||||
import { BehaviorSubject, combineLatest, debounceTime } from 'rxjs';
|
||||
import useResizeObserver, { type ObservedSize } from 'use-resize-observer/polyfilled';
|
||||
|
||||
import {
|
||||
ActivePanel,
|
||||
GridAccessMode,
|
||||
GridLayoutData,
|
||||
GridLayoutStateManager,
|
||||
GridSettings,
|
||||
PanelInteractionEvent,
|
||||
RuntimeGridSettings,
|
||||
} from './types';
|
||||
import { shouldShowMobileView } from './utils/mobile_view';
|
||||
|
||||
export const useGridLayoutState = ({
|
||||
layout,
|
||||
gridSettings,
|
||||
expandedPanelId,
|
||||
accessMode,
|
||||
}: {
|
||||
layout: GridLayoutData;
|
||||
gridSettings: GridSettings;
|
||||
expandedPanelId?: string;
|
||||
accessMode: GridAccessMode;
|
||||
}): {
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
setDimensionsRef: (instance: HTMLDivElement | null) => void;
|
||||
|
@ -34,6 +39,25 @@ export const useGridLayoutState = ({
|
|||
const rowRefs = useRef<Array<HTMLDivElement | null>>([]);
|
||||
const panelRefs = useRef<Array<{ [id: string]: HTMLDivElement | null }>>([]);
|
||||
|
||||
const expandedPanelId$ = useMemo(
|
||||
() => new BehaviorSubject<string | undefined>(expandedPanelId),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
useEffect(() => {
|
||||
expandedPanelId$.next(expandedPanelId);
|
||||
}, [expandedPanelId, expandedPanelId$]);
|
||||
|
||||
const accessMode$ = useMemo(
|
||||
() => new BehaviorSubject<GridAccessMode>(accessMode),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
accessMode$.next(accessMode);
|
||||
}, [accessMode, accessMode$]);
|
||||
|
||||
const gridLayoutStateManager = useMemo(() => {
|
||||
const gridLayout$ = new BehaviorSubject<GridLayoutData>(layout);
|
||||
const gridDimensions$ = new BehaviorSubject<ObservedSize>({ width: 0, height: 0 });
|
||||
|
@ -56,6 +80,8 @@ export const useGridLayoutState = ({
|
|||
gridDimensions$,
|
||||
runtimeSettings$,
|
||||
interactionEvent$,
|
||||
expandedPanelId$,
|
||||
isMobileView$: new BehaviorSubject<boolean>(shouldShowMobileView(accessMode)),
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
@ -64,15 +90,16 @@ export const useGridLayoutState = ({
|
|||
/**
|
||||
* debounce width changes to avoid unnecessary column width recalculation.
|
||||
*/
|
||||
const resizeSubscription = gridLayoutStateManager.gridDimensions$
|
||||
const resizeSubscription = combineLatest([gridLayoutStateManager.gridDimensions$, accessMode$])
|
||||
.pipe(debounceTime(250))
|
||||
.subscribe((dimensions) => {
|
||||
.subscribe(([dimensions, currentAccessMode]) => {
|
||||
const elementWidth = dimensions.width ?? 0;
|
||||
const columnPixelWidth =
|
||||
(elementWidth - gridSettings.gutterSize * (gridSettings.columnCount - 1)) /
|
||||
gridSettings.columnCount;
|
||||
|
||||
gridLayoutStateManager.runtimeSettings$.next({ ...gridSettings, columnPixelWidth });
|
||||
gridLayoutStateManager.isMobileView$.next(shouldShowMobileView(currentAccessMode));
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
|
17
packages/kbn-grid-layout/grid/utils/mobile_view.ts
Normal file
17
packages/kbn-grid-layout/grid/utils/mobile_view.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { GridAccessMode } from '../types';
|
||||
|
||||
const getViewportWidth = () =>
|
||||
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
|
||||
export const shouldShowMobileView = (accessMode: GridAccessMode) =>
|
||||
accessMode === 'VIEW' && getViewportWidth() < parseFloat(euiThemeVars.euiBreakpoints.m);
|
|
@ -34,7 +34,7 @@ const getAllCollisionsWithPanel = (
|
|||
return collidingPanels;
|
||||
};
|
||||
|
||||
const getKeysInOrder = (rowData: GridRowData, draggedId?: string): string[] => {
|
||||
export const getKeysInOrder = (rowData: GridRowData, draggedId?: string): string[] => {
|
||||
const panelKeys = Object.keys(rowData.panels);
|
||||
return panelKeys.sort((panelKeyA, panelKeyB) => {
|
||||
const panelA = rowData.panels[panelKeyA];
|
||||
|
|
|
@ -8,6 +8,12 @@
|
|||
*/
|
||||
|
||||
export { GridLayout } from './grid/grid_layout';
|
||||
export type { GridLayoutData, GridPanelData, GridRowData, GridSettings } from './grid/types';
|
||||
export type {
|
||||
GridLayoutData,
|
||||
GridPanelData,
|
||||
GridRowData,
|
||||
GridSettings,
|
||||
GridAccessMode,
|
||||
} from './grid/types';
|
||||
|
||||
export { isLayoutEqual } from './grid/utils/equality_checks';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue