[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:
Marta Bondyra 2024-11-28 19:31:09 +01:00 committed by GitHub
parent 650c5ca212
commit d92aa91c3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 315 additions and 39 deletions

View file

@ -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,

View file

@ -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
}, []);

View file

@ -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>

View file

@ -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

View file

@ -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" />
</>
);
};

View file

@ -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';

View file

@ -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 () => {

View 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);

View file

@ -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];

View file

@ -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';