mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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 | |--------|--------| |  |  | |  |  | - **Resizing** | Before | After | |--------|--------| |  |  | |  |  | 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:
parent
58113622ab
commit
5ee4297994
12 changed files with 309 additions and 151 deletions
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
}, []);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue