[Dashboard][kbn-grid-layout] Update styles (#206503)

Closes https://github.com/elastic/kibana/issues/204060

## Summary

This PR updates the styles used for `kbn-grid-layout` in Dashboard as
shown below.

- **Dragging**

    | Before | After |
    |--------|--------|
|
![image](https://github.com/user-attachments/assets/13161969-3eaf-4dce-bcf4-7b4850215816)
|
![image](https://github.com/user-attachments/assets/d76dc678-6277-4819-b554-f6b66b200c0c)
|
|
![image](https://github.com/user-attachments/assets/84d8d489-2240-4f10-809f-0aa30415f408)
|
![image](https://github.com/user-attachments/assets/573d71ad-71fb-47ab-a34e-66b845ecff67)
|

- **Resizing**

    | Before | After |
    |--------|--------|
|
![image](https://github.com/user-attachments/assets/79dfebd0-538b-4193-9b66-30961e9c7b21)
|
![image](https://github.com/user-attachments/assets/bc66ed35-83c4-4291-8cec-6ae8dda8f006)
|
|
![image](https://github.com/user-attachments/assets/d3fb5643-a77f-416f-9fc3-53af6225782a)
|
![image](https://github.com/user-attachments/assets/df2c65d5-af52-4848-b16c-f9f85abd5d9a)
|

As part of this work, I moved all aesthetic style logic out of the
`kbn-grid-layout` package and added support for Emotion to the
`GridLayout` component instead - this means that the consumer is
responsible for applying styles based on given classes, and
`kbn-grid-layout` is now less opinionated. The only styling kept in the
`kbn-grid-layout` package are those that handle layout-engine specific
functionality (positioning of panels, hiding edit actions in view mode,
etc).

In addition, I also updated the styles used in the grid example app and
added settings for dynamically changing the grid gutter size + row
height:




https://github.com/user-attachments/assets/c2f06db1-7041-412e-b546-86b102cc0770


### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [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 has minimal risk, since it is primarily style changes.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Hannah Mudge 2025-01-21 12:52:39 -07:00 committed by GitHub
parent 58113622ab
commit 5ee4297994
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 309 additions and 151 deletions

View file

@ -8,7 +8,7 @@
*/
import deepEqual from 'fast-deep-equal';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { combineLatest, debounceTime } from 'rxjs';
@ -20,9 +20,15 @@ import {
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPageTemplate,
EuiPopover,
EuiRange,
EuiSpacer,
transparentize,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { AppMountParameters } from '@kbn/core-application-browser';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import { AddEmbeddableButton } from '@kbn/embeddable-examples-plugin/public';
@ -53,11 +59,16 @@ export const GridExample = ({
coreStart: CoreStart;
uiActions: UiActionsStart;
}) => {
const { euiTheme } = useEuiTheme();
const savedState = useRef<MockSerializedDashboardState>(getSerializedDashboardState());
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
const [currentLayout, setCurrentLayout] = useState<GridLayoutData>(
dashboardInputToGridLayout(savedState.current)
);
const [isSettingsPopoverOpen, setIsSettingsPopoverOpen] = useState(false);
const [gutterSize, setGutterSize] = useState<number>(DASHBOARD_MARGIN_SIZE);
const [rowHeight, setRowHeight] = useState<number>(DASHBOARD_GRID_HEIGHT);
const mockDashboardApi = useMockDashboardApi({ savedState: savedState.current });
const [viewMode, expandedPanelId] = useBatchedPublishingSubjects(
@ -111,6 +122,41 @@ export const GridExample = ({
[mockDashboardApi]
);
const customLayoutCss = useMemo(() => {
const gridColor = transparentize(euiTheme.colors.backgroundFilledAccentSecondary, 0.2);
return css`
.kbnGridRow--targeted {
background-position: top calc((var(--kbnGridGutterSize) / 2) * -1px) left
calc((var(--kbnGridGutterSize) / 2) * -1px);
background-size: calc((var(--kbnGridColumnWidth) + var(--kbnGridGutterSize)) * 1px)
calc((var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) * 1px);
background-image: linear-gradient(to right, ${gridColor} 1px, transparent 1px),
linear-gradient(to bottom, ${gridColor} 1px, transparent 1px);
background-color: ${transparentize(euiTheme.colors.backgroundFilledAccentSecondary, 0.1)};
}
.kbnGridPanel--dragPreview {
border-radius: ${euiTheme.border.radius};
background-color: ${transparentize(euiTheme.colors.backgroundFilledAccentSecondary, 0.2)};
transition: opacity 100ms linear;
}
.kbnGridPanel--resizeHandle {
opacity: 0;
transition: opacity 0.2s, border 0.2s;
border-radius: 7px 0 7px 0;
border-bottom: 2px solid ${euiTheme.colors.accentSecondary};
border-right: 2px solid ${euiTheme.colors.accentSecondary};
&:hover,
&:focus {
outline-style: none !important;
opacity: 1;
background-color: ${transparentize(euiTheme.colors.accentSecondary, 0.05)};
}
}
`;
}, [euiTheme]);
return (
<KibanaRenderContextProvider {...coreStart}>
<EuiPageTemplate grow={false} offset={0} restrictWidth={false}>
@ -148,38 +194,96 @@ export const GridExample = ({
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<AddEmbeddableButton pageApi={mockDashboardApi} uiActions={uiActions} />
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<AddEmbeddableButton pageApi={mockDashboardApi} uiActions={uiActions} />{' '}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButton
iconType="arrowDown"
iconSide="right"
onClick={() => setIsSettingsPopoverOpen(!isSettingsPopoverOpen)}
>
{i18n.translate('examples.gridExample.settingsPopover.title', {
defaultMessage: 'Layout settings',
})}
</EuiButton>
}
isOpen={isSettingsPopoverOpen}
closePopover={() => setIsSettingsPopoverOpen(false)}
>
<>
<EuiFormRow
label={i18n.translate('examples.gridExample.settingsPopover.viewMode', {
defaultMessage: 'View mode',
})}
>
<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={viewMode}
onChange={(id) => {
mockDashboardApi.viewMode.next(id);
}}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('examples.gridExample.settingsPopover.gutterSize', {
defaultMessage: 'Gutter size',
})}
>
<EuiRange
min={1}
max={30}
value={gutterSize}
onChange={(e) => setGutterSize(parseInt(e.currentTarget.value, 10))}
showLabels
showValue
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('examples.gridExample.settingsPopover.rowHeight', {
defaultMessage: 'Row height',
})}
>
<EuiRange
min={5}
max={30}
step={5}
value={rowHeight}
onChange={(e) => setRowHeight(parseInt(e.currentTarget.value, 10))}
showLabels
showValue
/>
</EuiFormRow>
</>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</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={viewMode}
onChange={(id) => {
mockDashboardApi.viewMode.next(id);
}}
/>
</EuiFlexItem>
{hasUnsavedChanges && (
<EuiFlexItem grow={false}>
<EuiBadge color="warning">
@ -223,13 +327,14 @@ export const GridExample = ({
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<GridLayout
accessMode={viewMode === 'view' ? 'VIEW' : 'EDIT'}
expandedPanelId={expandedPanelId}
layout={currentLayout}
gridSettings={{
gutterSize: DASHBOARD_MARGIN_SIZE,
rowHeight: DASHBOARD_GRID_HEIGHT,
gutterSize,
rowHeight,
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
}}
renderPanelContents={renderPanelContents}
@ -241,6 +346,7 @@ export const GridExample = ({
mockDashboardApi.panels$.next(panels);
mockDashboardApi.rows$.next(rows);
}}
css={customLayoutCss}
/>
</EuiPageTemplate.Section>
</EuiPageTemplate>

View file

@ -10,7 +10,6 @@
import React, { useEffect, useRef } from 'react';
import { combineLatest, skip } from 'rxjs';
import { transparentize, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { GridLayoutStateManager } from './types';
@ -23,7 +22,6 @@ export const DragPreview = ({
gridLayoutStateManager: GridLayoutStateManager;
}) => {
const dragPreviewRef = useRef<HTMLDivElement | null>(null);
const { euiTheme } = useEuiTheme();
useEffect(
() => {
@ -59,12 +57,10 @@ export const DragPreview = ({
return (
<div
ref={dragPreviewRef}
className={'kbnGridPanel--dragPreview'}
css={css`
display: none;
pointer-events: none;
border-radius: ${euiTheme.border.radius};
background-color: ${transparentize(euiTheme.colors.accentSecondary, 0.2)};
transition: opacity 100ms linear;
`}
/>
);

View file

@ -9,7 +9,7 @@
import { css } from '@emotion/react';
import React, { PropsWithChildren, useEffect, useRef } from 'react';
import { combineLatest, distinctUntilChanged, map } from 'rxjs';
import { combineLatest } from 'rxjs';
import { GridLayoutStateManager } from './types';
export const GridHeightSmoother = ({
@ -32,35 +32,19 @@ export const GridHeightSmoother = ({
if (!smoothHeightRef.current || gridLayoutStateManager.expandedPanelId$.getValue()) return;
if (!interactionEvent) {
smoothHeightRef.current.style.height = `${dimensions.height}px`;
smoothHeightRef.current.style.minHeight = `${dimensions.height}px`;
smoothHeightRef.current.style.userSelect = 'auto';
return;
}
smoothHeightRef.current.style.height = `${Math.max(
dimensions.height ?? 0,
smoothHeightRef.current.style.minHeight = `${
smoothHeightRef.current.getBoundingClientRect().height
)}px`;
}px`;
smoothHeightRef.current.style.userSelect = 'none';
});
/**
* 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}`);
});
return () => {
interactionStyleSubscription.unsubscribe();
globalCssVariableSubscription.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -70,17 +54,14 @@ export const GridHeightSmoother = ({
ref={smoothHeightRef}
className={'kbnGridWrapper'}
css={css`
margin: calc(var(--kbnGridGutterSize) * 1px);
height: 100%;
overflow-anchor: none;
transition: height 500ms linear;
transition: min-height 500ms linear;
&:has(.kbnGridPanel--expanded) {
height: 100% !important;
min-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);
}
`}
>

View file

@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import classNames from 'classnames';
import { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { combineLatest, distinctUntilChanged, filter, map, pairwise, skip } from 'rxjs';
@ -31,6 +32,7 @@ export interface GridLayoutProps {
onLayoutChange: (newLayout: GridLayoutData) => void;
expandedPanelId?: string;
accessMode?: GridAccessMode;
className?: string; // this makes it so that custom CSS can be passed via Emotion
}
export const GridLayout = ({
@ -40,15 +42,17 @@ export const GridLayout = ({
onLayoutChange,
expandedPanelId,
accessMode = 'EDIT',
className,
}: GridLayoutProps) => {
const layoutRef = useRef<HTMLDivElement | null>(null);
const { gridLayoutStateManager, setDimensionsRef } = useGridLayoutState({
layout,
layoutRef,
gridSettings,
expandedPanelId,
accessMode,
});
useGridLayoutEvents({ gridLayoutStateManager });
const layoutRef = useRef<HTMLDivElement | null>(null);
const [rowCount, setRowCount] = useState<number>(
gridLayoutStateManager.gridLayout$.getValue().length
@ -173,8 +177,10 @@ export const GridLayout = ({
layoutRef.current = divElement;
setDimensionsRef(divElement);
}}
className="kbnGrid"
className={classNames('kbnGrid', className)}
css={css`
padding: calc(var(--kbnGridGutterSize) * 1px);
&:has(.kbnGridPanel--expanded) {
${expandedPanelStyles}
}

View file

@ -19,7 +19,6 @@ import {
UserMouseEvent,
UserTouchEvent,
} from '../types';
import { isMouseEvent, isTouchEvent } from '../utils/sensors';
export interface DragHandleApi {
setDragHandles: (refs: Array<HTMLElement | null>) => void;
@ -47,26 +46,13 @@ export const DragHandle = React.forwardRef<
*/
const onDragStart = useCallback(
(e: UserMouseEvent | UserTouchEvent) => {
// ignore when not in edit mode
if (gridLayoutStateManager.accessMode$.getValue() !== 'EDIT') return;
// ignore anything but left clicks for mouse events
if (isMouseEvent(e) && e.button !== 0) {
return;
}
// ignore multi-touch events for touch events
if (isTouchEvent(e) && e.touches.length > 1) {
return;
}
e.stopPropagation();
interactionStart('drag', e);
},
[interactionStart, gridLayoutStateManager.accessMode$]
[interactionStart]
);
const onDragEnd = useCallback(
(e: UserTouchEvent | UserMouseEvent) => {
e.stopPropagation();
interactionStart('drop', e);
},
[interactionStart]
@ -118,7 +104,7 @@ export const DragHandle = React.forwardRef<
aria-label={i18n.translate('kbnGridLayout.dragHandle.ariaLabel', {
defaultMessage: 'Drag to move',
})}
className="kbnGridPanel__dragHandle"
className="kbnGridPanel--dragHandle"
css={css`
opacity: 0;
display: flex;

View file

@ -68,13 +68,12 @@ export const GridPanel = forwardRef<HTMLDivElement, GridPanelProps>(
/** Set initial styles based on state at mount to prevent styles from "blipping" */
const initialStyles = useMemo(() => {
const initialPanel = gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels[panelId];
const { rowHeight } = gridLayoutStateManager.runtimeSettings$.getValue();
return css`
position: relative;
height: calc(
1px *
(
${initialPanel.height} * (${rowHeight} + var(--kbnGridGutterSize)) -
${initialPanel.height} * (var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) -
var(--kbnGridGutterSize)
)
);
@ -91,10 +90,9 @@ export const GridPanel = forwardRef<HTMLDivElement, GridPanelProps>(
const activePanelStyleSubscription = combineLatest([
gridLayoutStateManager.activePanel$,
gridLayoutStateManager.gridLayout$,
gridLayoutStateManager.runtimeSettings$,
])
.pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
.subscribe(([activePanel, gridLayout, runtimeSettings]) => {
.subscribe(([activePanel, gridLayout]) => {
const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
const panel = gridLayout[rowIndex].panels[panelId];
if (!ref || !panel) return;
@ -102,8 +100,11 @@ export const GridPanel = forwardRef<HTMLDivElement, GridPanelProps>(
const currentInteractionEvent = gridLayoutStateManager.interactionEvent$.getValue();
if (panelId === activePanel?.id) {
ref.classList.add('kbnGridPanel--active');
// if the current panel is active, give it fixed positioning depending on the interaction event
const { position: draggingPosition } = activePanel;
const runtimeSettings = gridLayoutStateManager.runtimeSettings$.getValue();
ref.style.zIndex = `${euiTheme.levels.modal}`;
if (currentInteractionEvent?.type === 'resize') {
@ -135,7 +136,7 @@ export const GridPanel = forwardRef<HTMLDivElement, GridPanelProps>(
ref.style.gridArea = `auto`; // shortcut to set all grid styles to `auto`
}
} else {
const { rowHeight } = gridLayoutStateManager.runtimeSettings$.getValue();
ref.classList.remove('kbnGridPanel--active');
ref.style.zIndex = `auto`;
@ -145,7 +146,7 @@ export const GridPanel = forwardRef<HTMLDivElement, GridPanelProps>(
ref.style.top = ``;
ref.style.width = ``;
// setting the height is necessary for mobile mode
ref.style.height = `calc(1px * (${panel.height} * (${rowHeight} + var(--kbnGridGutterSize)) - var(--kbnGridGutterSize)))`;
ref.style.height = `calc(1px * (${panel.height} * (var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) - var(--kbnGridGutterSize)))`;
// and render the panel locked to the grid
ref.style.gridColumnStart = `${panel.column + 1}`;

View file

@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { transparentize } from '@elastic/eui';
import { css } from '@emotion/react';
import { useEuiTheme } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -22,7 +21,7 @@ export const ResizeHandle = ({
const { euiTheme } = useEuiTheme();
return (
<button
className="kbnGridPanel__resizeHandle"
className="kbnGridPanel--resizeHandle"
onMouseDown={(e) => {
interactionStart('resize', e);
}}
@ -41,7 +40,6 @@ export const ResizeHandle = ({
css={css`
right: 0;
bottom: 0;
opacity: 0;
margin: -2px;
position: absolute;
width: ${euiTheme.size.l};
@ -49,28 +47,14 @@ export const ResizeHandle = ({
max-height: 100%;
height: ${euiTheme.size.l};
z-index: ${euiTheme.levels.toast};
transition: opacity 0.2s, border 0.2s;
border-radius: 7px 0 7px 0;
border-bottom: 2px solid ${euiTheme.colors.accentSecondary};
border-right: 2px solid ${euiTheme.colors.accentSecondary};
&:hover,
&:focus {
outline-style: none !important;
opacity: 1;
background-color: ${transparentize(euiTheme.colors.accentSecondary, 0.05)};
cursor: se-resize;
}
.kbnGrid--static &,
.kbnGridPanel--expanded & {
opacity: 0 !important;
display: none;
}
.kbnGridPanel__dragHandle:has(~ &:hover) {
opacity: 0 !important;
}
.kbnGridPanel__dragHandle:has(~ &:focus) {
opacity: 0 !important;
}
`}
/>
);

View file

@ -11,7 +11,6 @@ import { cloneDeep } from 'lodash';
import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
import { combineLatest, map, pairwise, skip } from 'rxjs';
import { transparentize, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { DragPreview } from '../drag_preview';
@ -42,17 +41,13 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
const [rowTitle, setRowTitle] = useState<string>(currentRow.title);
const [isCollapsed, setIsCollapsed] = useState<boolean>(currentRow.isCollapsed);
const { euiTheme } = useEuiTheme();
const rowContainer = useRef<HTMLDivElement | null>(null);
/** Set initial styles based on state at mount to prevent styles from "blipping" */
const initialStyles = useMemo(() => {
const runtimeSettings = gridLayoutStateManager.runtimeSettings$.getValue();
const { columnCount, rowHeight } = runtimeSettings;
const { columnCount } = gridLayoutStateManager.runtimeSettings$.getValue();
return css`
grid-auto-rows: ${rowHeight}px;
grid-auto-rows: calc(var(--kbnGridRowHeight) * 1px);
grid-template-columns: repeat(${columnCount}, minmax(0, 1fr));
gap: calc(var(--kbnGridGutterSize) * 1px);
`;
@ -63,38 +58,17 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
/** Update the styles of the grid row via a subscription to prevent re-renders */
const interactionStyleSubscription = combineLatest([
gridLayoutStateManager.interactionEvent$,
gridLayoutStateManager.gridLayout$,
gridLayoutStateManager.runtimeSettings$,
])
.pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
.subscribe(([interactionEvent, gridLayout, runtimeSettings]) => {
.subscribe(([interactionEvent]) => {
const rowRef = gridLayoutStateManager.rowRefs.current[rowIndex];
if (!rowRef) return;
const { gutterSize, rowHeight, columnPixelWidth } = runtimeSettings;
const targetRow = interactionEvent?.targetRowIndex;
if (rowIndex === targetRow && interactionEvent) {
// apply "targetted row" styles
const gridColor = euiTheme.colors.backgroundLightAccentSecondary;
rowRef.style.backgroundPosition = `top -${gutterSize / 2}px left -${
gutterSize / 2
}px`;
rowRef.style.backgroundSize = ` ${columnPixelWidth + gutterSize}px ${
rowHeight + gutterSize
}px`;
rowRef.style.backgroundImage = `linear-gradient(to right, ${gridColor} 1px, transparent 1px),
linear-gradient(to bottom, ${gridColor} 1px, transparent 1px)`;
rowRef.style.backgroundColor = `${transparentize(
euiTheme.colors.backgroundLightAccentSecondary,
0.25
)}`;
rowRef.classList.add('kbnGridRow--targeted');
} else {
// undo any "targetted row" styles
rowRef.style.backgroundPosition = ``;
rowRef.style.backgroundSize = ``;
rowRef.style.backgroundImage = ``;
rowRef.style.backgroundColor = `transparent`;
rowRef.classList.remove('kbnGridRow--targeted');
}
});
@ -157,12 +131,23 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
gridLayoutStateManager={gridLayoutStateManager}
renderPanelContents={renderPanelContents}
interactionStart={(type, e) => {
e.stopPropagation();
// Disable interactions when a panel is expanded
const isInteractive = gridLayoutStateManager.expandedPanelId$.value === undefined;
// ignore all interactions when panel is expanded or when not in edit mode
const isInteractive =
gridLayoutStateManager.expandedPanelId$.value === undefined &&
gridLayoutStateManager.accessMode$.getValue() === 'EDIT';
if (!isInteractive) return;
// ignore anything but left clicks for mouse events
if (isMouseEvent(e) && e.button !== 0) {
return;
}
// ignore multi-touch events for touch events
if (isTouchEvent(e) && e.touches.length > 1) {
return;
}
e.stopPropagation();
const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
if (!panelRef) return;

View file

@ -7,14 +7,13 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { useEuiTheme } from '@elastic/eui';
import deepEqual from 'fast-deep-equal';
import { cloneDeep, pick } from 'lodash';
import { useEffect, useMemo, useRef } from 'react';
import { BehaviorSubject, combineLatest, debounceTime } from 'rxjs';
import { BehaviorSubject, combineLatest, debounceTime, distinctUntilChanged } from 'rxjs';
import useResizeObserver, { type ObservedSize } from 'use-resize-observer/polyfilled';
import { useEuiTheme } from '@elastic/eui';
import {
ActivePanel,
GridAccessMode,
@ -29,11 +28,13 @@ import { resolveGridRow } from './utils/resolve_grid_row';
export const useGridLayoutState = ({
layout,
layoutRef,
gridSettings,
expandedPanelId,
accessMode,
}: {
layout: GridLayoutData;
layoutRef: React.MutableRefObject<HTMLDivElement | null>;
gridSettings: GridSettings;
expandedPanelId?: string;
accessMode: GridAccessMode;
@ -59,7 +60,6 @@ export const useGridLayoutState = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
useEffect(() => {
if (accessMode !== accessMode$.getValue()) accessMode$.next(accessMode);
}, [accessMode, accessMode$]);
@ -73,7 +73,6 @@ export const useGridLayoutState = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
useEffect(() => {
const runtimeSettings = runtimeSettings$.getValue();
if (!deepEqual(gridSettings, pick(runtimeSettings, ['gutterSize', 'rowHeight', 'columnCount'])))
@ -140,8 +139,21 @@ export const useGridLayoutState = ({
}
});
/**
* This subscription sets CSS variables that can be used by `layoutRef` and all of its children
*/
const cssVariableSubscription = gridLayoutStateManager.runtimeSettings$
.pipe(distinctUntilChanged(deepEqual))
.subscribe(({ gutterSize, columnPixelWidth, rowHeight }) => {
if (!layoutRef.current) return;
layoutRef.current.style.setProperty('--kbnGridGutterSize', `${gutterSize}`);
layoutRef.current.style.setProperty('--kbnGridRowHeight', `${rowHeight}`);
layoutRef.current.style.setProperty('--kbnGridColumnWidth', `${columnPixelWidth}`);
});
return () => {
resizeSubscription.unsubscribe();
cssVariableSubscription.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

View file

@ -2,12 +2,8 @@
position: relative;
}
/**
* 1. Need to override the react grid layout height when a single panel is expanded. Important is required because
* otherwise the height is set inline.
*/
.dshLayout-isMaximizedPanel {
height: 100%;
height: 100%; // need to override the kbn-grid-layout height when a single panel is expanded
.embPanel__hoverActionsLeft {
visibility: hidden;

View file

@ -12,17 +12,19 @@ import React, { useCallback, useMemo, useRef } from 'react';
import { useAppFixedViewport } from '@kbn/core-rendering-browser';
import { GridLayout, type GridLayoutData } from '@kbn/grid-layout';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { DashboardPanelState } from '../../../../common';
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../../common/content_management/constants';
import { arePanelLayoutsEqual } from '../../../dashboard_api/are_panel_layouts_equal';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
import { DASHBOARD_GRID_HEIGHT, DASHBOARD_MARGIN_SIZE } from './constants';
import { DashboardGridItem } from './dashboard_grid_item';
import { useLayoutStyles } from './use_layout_styles';
export const DashboardGrid = ({ dashboardContainer }: { dashboardContainer?: HTMLElement }) => {
const dashboardApi = useDashboardApi();
const layoutStyles = useLayoutStyles();
const panelRefs = useRef<{ [panelId: string]: React.Ref<HTMLDivElement> }>({});
const [expandedPanelId, panels, useMargins, viewMode] = useBatchedPublishingSubjects(
@ -112,6 +114,7 @@ export const DashboardGrid = ({ dashboardContainer }: { dashboardContainer?: HTM
// memoizing this component reduces the number of times it gets re-rendered to a minimum
return (
<GridLayout
css={layoutStyles}
layout={currentLayout}
gridSettings={{
gutterSize: useMargins ? DASHBOARD_MARGIN_SIZE : 0,
@ -124,7 +127,15 @@ export const DashboardGrid = ({ dashboardContainer }: { dashboardContainer?: HTM
accessMode={viewMode === 'edit' ? 'EDIT' : 'VIEW'}
/>
);
}, [currentLayout, useMargins, renderPanelContents, onLayoutChange, expandedPanelId, viewMode]);
}, [
layoutStyles,
currentLayout,
useMargins,
renderPanelContents,
onLayoutChange,
expandedPanelId,
viewMode,
]);
const classes = classNames({
'dshLayout-withoutMargins': !useMargins,

View file

@ -0,0 +1,94 @@
/*
* 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 { transparentize, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { useMemo } from 'react';
export const useLayoutStyles = () => {
const { euiTheme } = useEuiTheme();
const layoutStyles = useMemo(() => {
const getRadialGradient = (position: string) => {
return `radial-gradient(
circle at ${position},
${euiTheme.colors.accentSecondary} 1px,
transparent 1px
)`;
};
/**
* TODO: We are currently using `euiTheme.colors.vis.euiColorVis0` for grid layout styles because it
* is the best choice available; however, once https://github.com/elastic/platform-ux-team/issues/586
* is resolved, we should swap these out for the drag-specific colour tokens
*/
return css`
&.kbnGrid {
// remove margin top + bottom on grid in favour of padding in row
padding-bottom: 0px;
}
.kbnGridRow {
// use padding in grid row so that dotted grid is not cut off
padding-bottom: calc(var(--kbnGridGutterSize) * 1px);
&--targeted {
background-position: top calc((var(--kbnGridGutterSize) / 2) * -1px) left
calc((var(--kbnGridGutterSize) / 2) * -1px);
background-size: calc((var(--kbnGridColumnWidth) + var(--kbnGridGutterSize)) * 1px)
calc((var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) * 1px);
background-image: ${getRadialGradient('top left')}, ${getRadialGradient('top right')},
${getRadialGradient('bottom left')}, ${getRadialGradient('bottom right')};
background-origin: content-box;
}
}
.kbnGridPanel--dragPreview {
background-color: ${transparentize(euiTheme.colors.vis.euiColorVis0, 0.2)};
}
.kbnGridPanel--resizeHandle {
z-index: ${euiTheme.levels.mask};
// applying mask via ::after allows for focus borders to show
&:after {
display: block;
width: 100%;
height: 100%;
content: '';
mask-repeat: no-repeat;
mask-position: bottom ${euiTheme.size.s} right ${euiTheme.size.s};
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8' fill='none'%3E%3Cg clip-path='url(%23clip0_472_172810)'%3E%3Ccircle cx='7' cy='1' r='1' fill='%23000000'/%3E%3C/g%3E%3Cg clip-path='url(%23clip1_472_172810)'%3E%3Ccircle cx='4' cy='4' r='1' fill='%23000000'/%3E%3Ccircle cx='7' cy='4' r='1' fill='%23000000'/%3E%3C/g%3E%3Cg clip-path='url(%23clip2_472_172810)'%3E%3Ccircle cx='1' cy='7' r='1' fill='%23000000'/%3E%3Ccircle cx='4' cy='7' r='1' fill='%23000000'/%3E%3Ccircle cx='7' cy='7' r='1' fill='%23000000'/%3E%3C/g%3E%3C/svg%3E");
background-color: ${euiTheme.colors.borderBaseFormsControl};
}
&:hover,
&:focus-visible {
&:after {
background-color: ${euiTheme.colors.vis.euiColorVis0};
}
}
}
.kbnGridPanel--active {
.embPanel {
outline: ${euiTheme.border.width.thick} solid ${euiTheme.colors.vis.euiColorVis0} !important;
}
.embPanel__hoverActions {
border: ${euiTheme.border.width.thick} solid ${euiTheme.colors.vis.euiColorVis0} !important;
border-bottom: 0px solid !important;
}
}
`;
}, [euiTheme]);
return layoutStyles;
};