From e804b83451a152d76fa135e839fe68ea844ac03d Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 21 Nov 2024 12:21:05 -0700 Subject: [PATCH 1/2] First step of POC --- .../grid/grid_height_smoother.tsx | 22 +- packages/kbn-grid-layout/grid/grid_layout.tsx | 5 +- packages/kbn-grid-layout/grid/grid_panel.tsx | 213 ++++++++++++------ packages/kbn-grid-layout/grid/grid_row.tsx | 61 +++-- .../grid/use_grid_layout_events.ts | 5 +- .../_dashboard_container.scss | 1 + .../component/grid/_dashboard_grid.scss | 30 ++- .../component/grid/_dashboard_panel.scss | 4 + .../component/grid/dashboard_grid.tsx | 205 ++++++++++------- .../component/grid/dashboard_grid_item.tsx | 6 +- .../viewport/_dashboard_viewport.scss | 1 + .../react_embeddable_renderer.tsx | 1 + .../presentation_panel_header.tsx | 4 +- .../presentation_panel_hover_actions.tsx | 32 +-- .../presentation_panel_internal.tsx | 29 ++- .../public/panel_component/types.ts | 2 + 16 files changed, 415 insertions(+), 206 deletions(-) diff --git a/packages/kbn-grid-layout/grid/grid_height_smoother.tsx b/packages/kbn-grid-layout/grid/grid_height_smoother.tsx index 960fe4f52e735..078673bd7c77c 100644 --- a/packages/kbn-grid-layout/grid/grid_height_smoother.tsx +++ b/packages/kbn-grid-layout/grid/grid_height_smoother.tsx @@ -9,7 +9,7 @@ import { css } from '@emotion/react'; import React, { PropsWithChildren, useEffect, useRef } from 'react'; -import { combineLatest } from 'rxjs'; +import { combineLatest, distinctUntilChanged, map } from 'rxjs'; import { GridLayoutStateManager } from './types'; export const GridHeightSmoother = ({ @@ -19,13 +19,14 @@ export const GridHeightSmoother = ({ // set the parent div size directly to smooth out height changes. const smoothHeightRef = useRef(null); useEffect(() => { - const subscription = combineLatest([ + const heightSubscription = combineLatest([ gridLayoutStateManager.gridDimensions$, gridLayoutStateManager.interactionEvent$, ]).subscribe(([dimensions, interactionEvent]) => { if (!smoothHeightRef.current) return; if (!interactionEvent) { smoothHeightRef.current.style.height = `${dimensions.height}px`; + smoothHeightRef.current.style.userSelect = 'auto'; return; } @@ -38,8 +39,23 @@ export const GridHeightSmoother = ({ dimensions.height ?? 0, smoothHeightRef.current.getBoundingClientRect().height )}px`; + smoothHeightRef.current.style.userSelect = 'none'; }); - return () => subscription.unsubscribe(); + + const marginSubscription = gridLayoutStateManager.runtimeSettings$ + .pipe( + map(({ gutterSize }) => gutterSize), + distinctUntilChanged() + ) + .subscribe((gutterSize) => { + if (!smoothHeightRef.current) return; + smoothHeightRef.current.style.margin = `${gutterSize}px`; + }); + + return () => { + marginSubscription.unsubscribe(); + heightSubscription.unsubscribe(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/packages/kbn-grid-layout/grid/grid_layout.tsx b/packages/kbn-grid-layout/grid/grid_layout.tsx index fc67c5b134606..279ffe78e92da 100644 --- a/packages/kbn-grid-layout/grid/grid_layout.tsx +++ b/packages/kbn-grid-layout/grid/grid_layout.tsx @@ -22,7 +22,10 @@ import { compactGridRow } from './utils/resolve_grid_row'; interface GridLayoutProps { layout: GridLayoutData; gridSettings: GridSettings; - renderPanelContents: (panelId: string) => React.ReactNode; + renderPanelContents: ( + panelId: string, + setDragHandles: (refs: Array) => void + ) => React.ReactNode; onLayoutChange: (newLayout: GridLayoutData) => void; } diff --git a/packages/kbn-grid-layout/grid/grid_panel.tsx b/packages/kbn-grid-layout/grid/grid_panel.tsx index a44a321a7b18d..bbc7fc4952583 100644 --- a/packages/kbn-grid-layout/grid/grid_panel.tsx +++ b/packages/kbn-grid-layout/grid/grid_panel.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { forwardRef, useEffect, useMemo } from 'react'; +import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { combineLatest, skip } from 'rxjs'; import { @@ -28,10 +28,14 @@ export const GridPanel = forwardRef< { panelId: string; rowIndex: number; - renderPanelContents: (panelId: string) => React.ReactNode; + renderPanelContents: ( + panelId: string, + setDragHandles: (refs: Array) => void + ) => React.ReactNode; interactionStart: ( + panelId: string, type: PanelInteractionEvent['type'] | 'drop', - e: React.MouseEvent + e: MouseEvent | React.MouseEvent ) => void; gridLayoutStateManager: GridLayoutStateManager; } @@ -42,6 +46,9 @@ export const GridPanel = forwardRef< ) => { const { euiTheme } = useEuiTheme(); + const removeEventListenersRef = useRef<(() => void) | null>(null); + const [dragHandleCount, setDragHandleCount] = useState(0); + /** Set initial styles based on state at mount to prevent styles from "blipping" */ const initialStyles = useMemo(() => { const initialPanel = gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels[panelId]; @@ -63,6 +70,7 @@ export const GridPanel = forwardRef< ]) .pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it .subscribe(([activePanel, gridLayout, runtimeSettings]) => { + // console.log('SUBSCRIBE!!!!'); const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId]; const panel = gridLayout[rowIndex].panels[panelId]; if (!ref || !panel) return; @@ -87,31 +95,40 @@ export const GridPanel = forwardRef< // undo any "lock to grid" styles **except** for the top left corner, which stays locked ref.style.gridColumnStart = `${panel.column + 1}`; ref.style.gridRowStart = `${panel.row + 1}`; - ref.style.gridColumnEnd = ``; - ref.style.gridRowEnd = ``; + ref.style.gridColumnEnd = `auto`; + ref.style.gridRowEnd = `auto`; + + // if (resizeHandleRef.current) { + // resizeHandleRef.current.style.width = `${Math.min( + // 24, + // runtimeSettings.columnPixelWidth * panel.width + // )}px`; + // } } else { // if the current panel is being dragged, render it with a fixed position + size ref.style.position = 'fixed'; + + ref.classList.add('react-draggable-dragging'); + ref.style.left = `${draggingPosition.left}px`; ref.style.top = `${draggingPosition.top}px`; ref.style.width = `${draggingPosition.right - draggingPosition.left}px`; ref.style.height = `${draggingPosition.bottom - draggingPosition.top}px`; // undo any "lock to grid" styles - ref.style.gridColumnStart = ``; - ref.style.gridRowStart = ``; - ref.style.gridColumnEnd = ``; - ref.style.gridRowEnd = ``; + ref.style.gridArea = `auto`; // shortcut to set all grid styles to `auto` } } else { - ref.style.zIndex = '0'; + ref.style.zIndex = `auto`; // if the panel is not being dragged and/or resized, undo any fixed position styles - ref.style.position = ''; - ref.style.left = ``; - ref.style.top = ``; - ref.style.width = ``; - ref.style.height = ``; + ref.style.position = 'static'; + ref.style.left = `auto`; + ref.style.top = `auto`; + ref.style.width = `auto`; + ref.style.height = `auto`; + + ref.classList.remove('react-draggable-dragging'); // and render the panel locked to the grid ref.style.gridColumnStart = `${panel.column + 1}`; @@ -129,67 +146,143 @@ export const GridPanel = forwardRef< [] ); + useEffect(() => { + const onDropEventHandler = (dropEvent: MouseEvent) => + interactionStart(panelId, 'drop', dropEvent); + /** + * Subscription to add a singular "drop" event handler whenever an interaction starts - + * this is handled in a subscription so that it is not lost when the component gets remounted + * (which happens when a panel gets dragged from one grid row to another) + */ + const dropEventSubscription = gridLayoutStateManager.interactionEvent$.subscribe((event) => { + if (!event || event.id !== panelId) return; + + /** + * By adding the "drop" event listener to the document rather than the drag/resize event handler, + * we prevent the element from getting "stuck" in an interaction; however, we only attach this event + * listener **when the drag/resize event starts**, and it only executes once, which means we don't + * have to remove the `mouseup` event listener + */ + document.addEventListener('mouseup', onDropEventHandler, { + once: true, + passive: true, + }); + }); + + return () => { + dropEventSubscription.unsubscribe(); + document.removeEventListener('mouseup', onDropEventHandler); // removes the event listener on row change + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [panelId]); + + /** + * We need to memoize the `onMouseDown` callback so that we don't assign a new `onMouseDown` event handler + * every time `setDragHandles` is called + */ + const onMouseDown = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + interactionStart(panelId, 'drag', e); + }, + [panelId, interactionStart] + ); + + const setDragHandles = useCallback( + (dragHandles: Array) => { + setDragHandleCount(dragHandles.length); + + for (const handle of dragHandles) { + if (handle === null) return; + handle.addEventListener('mousedown', onMouseDown, { passive: true }); + } + + removeEventListenersRef.current = () => { + for (const handle of dragHandles) { + if (handle === null) return; + handle.removeEventListener('mousedown', onMouseDown); + } + }; + }, + [onMouseDown] + ); + /** * Memoize panel contents to prevent unnecessary re-renders */ const panelContents = useMemo(() => { - return renderPanelContents(panelId); - }, [panelId, renderPanelContents]); + return renderPanelContents(panelId, setDragHandles); + }, [panelId, renderPanelContents, setDragHandles]); + + useEffect(() => { + return () => { + if (removeEventListenersRef.current) { + removeEventListenersRef.current(); + } + }; + }, []); return ( <>
- {/* drag handle */} -
interactionStart('drag', e)} - onMouseUp={(e) => interactionStart('drop', e)} - > - -
+ {!dragHandleCount && ( +
interactionStart(panelId, 'drag', e)} + > + +
+ )} {/* Resize handle */}
interactionStart('resize', e)} - onMouseUp={(e) => interactionStart('drop', e)} + onMouseDown={(e) => interactionStart(panelId, 'resize', e)} css={css` right: 0; bottom: 0; opacity: 0; margin: -2px; + z-index: 9000; position: absolute; width: ${euiThemeVars.euiSizeL}; height: ${euiThemeVars.euiSizeL}; @@ -204,16 +297,8 @@ export const GridPanel = forwardRef< } `} /> -
- {panelContents} -
- + {panelContents} +
); diff --git a/packages/kbn-grid-layout/grid/grid_row.tsx b/packages/kbn-grid-layout/grid/grid_row.tsx index 01466a440b4cd..32caf448926eb 100644 --- a/packages/kbn-grid-layout/grid/grid_row.tsx +++ b/packages/kbn-grid-layout/grid/grid_row.tsx @@ -24,7 +24,10 @@ export const GridRow = forwardRef< { rowIndex: number; toggleIsCollapsed: () => void; - renderPanelContents: (panelId: string) => React.ReactNode; + renderPanelContents: ( + panelId: string, + setDragHandles: (refs: Array) => void + ) => React.ReactNode; setInteractionEvent: (interactionData?: PanelInteractionEvent) => void; gridLayoutStateManager: GridLayoutStateManager; } @@ -155,6 +158,37 @@ export const GridRow = forwardRef< [rowIndex] ); + const interactionStart = useCallback( + ( + panelId: string, + type: PanelInteractionEvent['type'] | 'drop', + e: MouseEvent | React.MouseEvent + ) => { + e.stopPropagation(); + const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId]; + if (!panelRef) return; + + const panelRect = panelRef.getBoundingClientRect(); + if (type === 'drop') { + setInteractionEvent(undefined); + } else { + setInteractionEvent({ + type, + id: panelId, + panelDiv: panelRef, + targetRowIndex: rowIndex, + mouseOffsets: { + top: e.clientY - panelRect.top, + left: e.clientX - panelRect.left, + right: e.clientX - panelRect.right, + bottom: e.clientY - panelRect.bottom, + }, + }); + } + }, + [] + ); + /** * Memoize panel children components to prevent unnecessary re-renders */ @@ -166,30 +200,7 @@ export const GridRow = forwardRef< rowIndex={rowIndex} gridLayoutStateManager={gridLayoutStateManager} renderPanelContents={renderPanelContents} - interactionStart={(type, e) => { - e.preventDefault(); - e.stopPropagation(); - const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId]; - if (!panelRef) return; - - const panelRect = panelRef.getBoundingClientRect(); - if (type === 'drop') { - setInteractionEvent(undefined); - } else { - setInteractionEvent({ - type, - id: panelId, - panelDiv: panelRef, - targetRowIndex: rowIndex, - mouseOffsets: { - top: e.clientY - panelRect.top, - left: e.clientX - panelRect.left, - right: e.clientX - panelRect.right, - bottom: e.clientY - panelRect.bottom, - }, - }); - } - }} + interactionStart={interactionStart} ref={(element) => { if (!gridLayoutStateManager.panelRefs.current[rowIndex]) { gridLayoutStateManager.panelRefs.current[rowIndex] = {}; diff --git a/packages/kbn-grid-layout/grid/use_grid_layout_events.ts b/packages/kbn-grid-layout/grid/use_grid_layout_events.ts index 22dde2fe68ced..8cf56c2cc37e1 100644 --- a/packages/kbn-grid-layout/grid/use_grid_layout_events.ts +++ b/packages/kbn-grid-layout/grid/use_grid_layout_events.ts @@ -28,7 +28,6 @@ export const useGridLayoutEvents = ({ const { runtimeSettings$, interactionEvent$, gridLayout$ } = gridLayoutStateManager; const calculateUserEvent = (e: Event) => { if (!interactionEvent$.value) return; - e.preventDefault(); e.stopPropagation(); const gridRowElements = gridLayoutStateManager.rowRefs.current; @@ -157,8 +156,8 @@ export const useGridLayoutEvents = ({ calculateUserEvent(e); }; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('scroll', calculateUserEvent); + document.addEventListener('mousemove', onMouseMove, { passive: true }); + document.addEventListener('scroll', calculateUserEvent, { passive: true }); return () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('scroll', calculateUserEvent); diff --git a/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss b/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss index e834a65230e81..02d6495b696dc 100644 --- a/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss +++ b/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss @@ -4,6 +4,7 @@ .dashboardContainer, .dashboardViewport { flex: auto; display: flex; + width: 100%; } .dashboardViewport--loading { diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss index ce010aa4cf9a5..b5d7caecb7e37 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss @@ -78,22 +78,28 @@ // REACT-GRID -.react-grid-item { - /** - * Copy over and overwrite the fill color with EUI color mixin (for theming) - */ - > .react-resizable-handle { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6' viewBox='0 0 6 6'%3E%3Cpolygon fill='#{hexToRGB($euiColorDarkShade)}' points='6 6 0 6 0 4.2 4 4.2 4.2 4.2 4.2 0 6 0' /%3E%3C/svg%3E%0A"); - - &::after { - border: none !important; /** overrides library default visual indicator **/ +.react-draggable-dragging { + transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance; + @include euiBottomShadowLarge; + border-radius: $euiBorderRadius; // keeps shadow within bounds + + .embPanel__hoverActionsWrapper { + z-index: $euiZLevel9; + top: -$euiSizeXL; + + // Show hover actions with drag handle + .embPanel__hoverActions:has(.embPanel--dragHandle) { + opacity: 1; } - &:hover, - &:focus { - background-color: transparentize($euiColorWarning, lightOrDarkTheme(.9, .7)); + // Hide hover actions without drag handle + .embPanel__hoverActions:not(:has(.embPanel--dragHandle)) { + opacity: 0; } } +} + +.react-grid-item { /** * Dragged/Resized panels in dashboard should always appear above other panels diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss index 2b7ec068f827d..80627f5a54860 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss @@ -59,3 +59,7 @@ z-index: $euiZLevel2; } } + +.dshDashboardGrid__item { + height: 100%; +} \ No newline at end of file diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx index f04ea2a698dd3..fc1ee0622d3e8 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx @@ -12,8 +12,8 @@ import 'react-grid-layout/css/styles.css'; import { pick } from 'lodash'; import classNames from 'classnames'; -import React, { useState, useMemo, useCallback, useEffect } from 'react'; -import { Layout, Responsive as ResponsiveReactGridLayout } from 'react-grid-layout'; +import React, { useState, useMemo, useCallback, useEffect, Profiler } from 'react'; +import { GridLayout, type GridLayoutData } from '@kbn/grid-layout'; import { ViewMode } from '@kbn/embeddable-plugin/public'; @@ -28,7 +28,11 @@ import { useDashboardGridSettings } from './use_dashboard_grid_settings'; import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api'; import { arePanelLayoutsEqual } from '../../../dashboard_api/are_panel_layouts_equal'; import { useDashboardInternalApi } from '../../../dashboard_api/use_dashboard_internal_api'; -import { DASHBOARD_GRID_HEIGHT, DASHBOARD_MARGIN_SIZE } from '../../../dashboard_constants'; +import { + DASHBOARD_GRID_COLUMN_COUNT, + DASHBOARD_GRID_HEIGHT, + DASHBOARD_MARGIN_SIZE, +} from '../../../dashboard_constants'; export const DashboardGrid = ({ dashboardContainer, @@ -52,6 +56,48 @@ export const DashboardGrid = ({ dashboardApi.viewMode ); + // const [currentLayout, setCurrentLayout] = useState(() => { + // const singleRow: GridLayoutData[number] = { + // title: 'First row', + // isCollapsed: false, + // panels: {}, + // }; + + // Object.keys(panels).forEach((panelId) => { + // const gridData = panels[panelId].gridData; + // singleRow.panels[panelId] = { + // id: panelId, + // row: gridData.y, + // column: gridData.x, + // width: gridData.w, + // height: gridData.h, + // }; + // }); + + // return [singleRow] as GridLayoutData; + // }); + + const currentLayout: GridLayoutData = useMemo(() => { + const singleRow: GridLayoutData[number] = { + title: 'First row', + isCollapsed: false, + panels: {}, + }; + + Object.keys(panels).forEach((panelId) => { + const gridData = panels[panelId].gridData; + singleRow.panels[panelId] = { + id: panelId, + row: gridData.y, + column: gridData.x, + width: gridData.w, + height: gridData.h, + }; + }); + + return [singleRow]; + }, [panels]); + /** * Track panel maximized state delayed by one tick and use it to prevent * panel sliding animations on maximize and minimize. @@ -67,67 +113,33 @@ export const DashboardGrid = ({ const appFixedViewport = useAppFixedViewport(); - const panelsInOrder: string[] = useMemo(() => { - return Object.keys(panels).sort((embeddableIdA, embeddableIdB) => { - const panelA = panels[embeddableIdA]; - const panelB = panels[embeddableIdB]; - - // need to manually sort the panels by position because we want the panels to be collapsed from the left to the - // right when switching to the single column layout, but RGL sorts by ID which can cause unexpected behaviour between - // by-reference and by-value panels + we want the HTML order to align with this in the multi-panel view - if (panelA.gridData.y === panelB.gridData.y) { - return panelA.gridData.x - panelB.gridData.x; - } else { - return panelA.gridData.y - panelB.gridData.y; - } - }); - }, [panels]); - - const panelComponents = useMemo(() => { - return panelsInOrder.map((embeddableId, index) => { - const type = panels[embeddableId].type; - return ( - - ); - }); - }, [ - appFixedViewport, - dashboardContainer, - expandedPanelId, - panels, - panelsInOrder, - focusedPanelId, - ]); - const onLayoutChange = useCallback( - (newLayout: Array) => { + (newLayout: GridLayoutData) => { if (viewMode !== ViewMode.EDIT) return; - - const updatedPanels: { [key: string]: DashboardPanelState } = newLayout.reduce( - (updatedPanelsAcc, panelLayout) => { - updatedPanelsAcc[panelLayout.i] = { - ...panels[panelLayout.i], - gridData: pick(panelLayout, ['x', 'y', 'w', 'h', 'i']), - }; - return updatedPanelsAcc; - }, - {} as { [key: string]: DashboardPanelState } - ); - if (!arePanelLayoutsEqual(panels, updatedPanels)) { + console.log('ON LAYOUT CHANGE', newLayout[0]); + + const currentPanels = dashboardApi.panels$.getValue(); + const updatedPanels: { [key: string]: DashboardPanelState } = Object.values( + newLayout[0].panels + ).reduce((updatedPanelsAcc, panelLayout) => { + updatedPanelsAcc[panelLayout.id] = { + ...currentPanels[panelLayout.id], + gridData: { + i: panelLayout.id, + y: panelLayout.row, + x: panelLayout.column, + w: panelLayout.width, + h: panelLayout.height, + }, + }; + return updatedPanelsAcc; + }, {} as { [key: string]: DashboardPanelState }); + if (!arePanelLayoutsEqual(currentPanels, updatedPanels)) { dashboardApi.setPanels(updatedPanels); } + // setCurrentLayout(newLayout); }, - [dashboardApi, panels, viewMode] + [dashboardApi, viewMode] ); const classes = classNames({ @@ -138,29 +150,64 @@ export const DashboardGrid = ({ 'dshLayout-isMaximizedPanel': expandedPanelId !== undefined, }); - const { layouts, breakpoints, columns } = useDashboardGridSettings(panelsInOrder, panels); + const renderPanelContents = useCallback( + (id: string, setDragHandles: (refs: Array) => void) => { + const currentPanels = dashboardApi.panels$.getValue(); + if (!currentPanels[id]) return; + console.log('renderPanelContents'); - // in print mode, dashboard layout is not controlled by React Grid Layout - if (viewMode === ViewMode.PRINT) { - return <>{panelComponents}; - } + const type = currentPanels[id].type; + return ( + + ); + }, + [expandedPanelId, focusedPanelId, appFixedViewport, dashboardApi] + ); + + const gridSettings = useMemo(() => { + return { + gutterSize: DASHBOARD_MARGIN_SIZE, + rowHeight: DASHBOARD_GRID_HEIGHT, + columnCount: DASHBOARD_GRID_COLUMN_COUNT, + }; + }, []); + + // // in print mode, dashboard layout is not controlled by React Grid Layout + // if (viewMode === ViewMode.PRINT) { + // return <>{panelComponents}; + // } return ( - { + console.log('on render', { + id, + phase, + actualDuration, + baseDuration, + startTime, + commitTime, + }); + }} > - {panelComponents} - +
+ +
+ ); }; diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx index ded3cc7095407..f1fc364832330 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx @@ -33,6 +33,7 @@ export interface Props extends DivProps { focusedPanelId?: string; key: string; isRenderable?: boolean; + setDragHandles: (refs: Array) => void; } export const Item = React.forwardRef( @@ -46,6 +47,7 @@ export const Item = React.forwardRef( index, type, isRenderable = true, + setDragHandles, // The props below are passed from ReactGridLayoutn and need to be merged with their counterparts. // https://github.com/react-grid-layout/react-grid-layout/issues/1241#issuecomment-658306889 children, @@ -113,11 +115,13 @@ export const Item = React.forwardRef( `; const renderedEmbeddable = useMemo(() => { + console.log('renderedEmbeddable'); const panelProps = { showBadges: true, showBorder: useMargins, showNotifications: true, showShadow: false, + setDragHandles, }; return ( @@ -133,7 +137,7 @@ export const Item = React.forwardRef( onApiAvailable={(api) => dashboardInternalApi.registerChildApi(api)} /> ); - }, [id, dashboardApi, dashboardInternalApi, type, useMargins]); + }, [id, dashboardApi, dashboardInternalApi, type, useMargins, setDragHandles]); return (
; hidePanelChrome?: boolean; /** diff --git a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_header.tsx b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_header.tsx index 0747e4a4f8229..4dc2879ccec3f 100644 --- a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_header.tsx +++ b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_header.tsx @@ -23,6 +23,7 @@ export type PresentationPanelHeaderProps; } & Pick; export const PresentationPanelHeader = < @@ -35,6 +36,7 @@ export const PresentationPanelHeader = < hideTitle, panelTitle, panelDescription, + dragHandleRef, showBadges = true, showNotifications = true, }: PresentationPanelHeaderProps) => { @@ -71,7 +73,7 @@ export const PresentationPanelHeader = < className={headerClasses} data-test-subj={`embeddablePanelHeading-${(panelTitle || '').replace(/\s/g, '')}`} > -

+

{ariaLabelElement} ; }) => { const [quickActions, setQuickActions] = useState([]); const [contextMenuPanels, setContextMenuPanels] = useState([]); @@ -125,7 +127,6 @@ export const PresentationPanelHoverActions = ({ const [notifications, setNotifications] = useState([]); const hoverActionsRef = useRef(null); const anchorRef = useRef(null); - const leftHoverActionsRef = useRef(null); const rightHoverActionsRef = useRef(null); const [combineHoverActions, setCombineHoverActions] = useState(false); const [borderStyles, setBorderStyles] = useState(TOP_ROUNDED_CORNERS); @@ -138,7 +139,7 @@ export const PresentationPanelHoverActions = ({ const anchorWidth = anchorRef.current.offsetWidth; const hoverActionsWidth = (rightHoverActionsRef.current?.offsetWidth ?? 0) + - (leftHoverActionsRef.current?.offsetWidth ?? 0) + + (dragHandleRef.current?.offsetWidth ?? 0) + parseInt(euiThemeVars.euiSize, 10) * 2; const hoverActionsHeight = rightHoverActionsRef.current?.offsetHeight ?? 0; @@ -443,18 +444,20 @@ export const PresentationPanelHoverActions = ({ ); const dragHandle = ( - +
+ +
); const hasHoverActions = quickActionElements.length || contextMenuPanels.lastIndexOf.length; @@ -535,7 +538,6 @@ export const PresentationPanelHoverActions = ({ > {viewMode === 'edit' && !combineHoverActions ? (
(null); const headerId = useMemo(() => htmlIdGenerator()(), []); + const dragHandleRefs = useRef<{ [key: string]: HTMLElement | null }>({}); + + const setDragHandle = useCallback( + (node: HTMLElement | null) => { + dragHandleRefs.current.test = node; + console.log('SET DRAG HANDLES', node); + setDragHandles?.(Object.values(dragHandleRefs.current).filter((value) => Boolean(value))); + }, + [setDragHandles] + ); + const viewModeSubject = (() => { if (apiPublishesViewMode(api)) return api.viewMode; if (apiHasParentApi(api) && apiPublishesViewMode(api.parentApi)) return api.parentApi.viewMode; @@ -92,7 +105,17 @@ export const PresentationPanelInternal = < return ( setDragHandle('hoverAction', node), + }} > setDragHandle('header', node)} + dragHandleRef={setDragHandle} /> )} {blockingError && api && ( diff --git a/src/plugins/presentation_panel/public/panel_component/types.ts b/src/plugins/presentation_panel/public/panel_component/types.ts index fa60f134321ac..df4126115c90c 100644 --- a/src/plugins/presentation_panel/public/panel_component/types.ts +++ b/src/plugins/presentation_panel/public/panel_component/types.ts @@ -42,6 +42,8 @@ export interface PresentationPanelInternalProps< showBadges?: boolean; showNotifications?: boolean; + setDragHandles?: (refs: Array) => void; + /** * Set to true to not show PanelLoader component while Panel is loading */ From 9ee1d98afa492303cbb728192fd36a2b08a88eaa Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 22 Nov 2024 16:28:54 -0700 Subject: [PATCH 2/2] Try re-adding grid overlay --- packages/kbn-grid-layout/grid/grid_layout.tsx | 22 +-- .../kbn-grid-layout/grid/grid_overlay.tsx | 146 ++++++++++++++++++ packages/kbn-grid-layout/grid/grid_row.tsx | 4 +- 3 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 packages/kbn-grid-layout/grid/grid_overlay.tsx diff --git a/packages/kbn-grid-layout/grid/grid_layout.tsx b/packages/kbn-grid-layout/grid/grid_layout.tsx index 279ffe78e92da..9c4e0420caba2 100644 --- a/packages/kbn-grid-layout/grid/grid_layout.tsx +++ b/packages/kbn-grid-layout/grid/grid_layout.tsx @@ -12,6 +12,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { combineLatest, distinctUntilChanged, filter, map, pairwise, skip } from 'rxjs'; import { GridHeightSmoother } from './grid_height_smoother'; +import { GridOverlay } from './grid_overlay'; import { GridRow } from './grid_row'; import { GridLayoutData, GridSettings } from './types'; import { useGridLayoutEvents } from './use_grid_layout_events'; @@ -136,14 +137,17 @@ export const GridLayout = ({ }, [rowCount, gridLayoutStateManager, renderPanelContents]); return ( - -
{ - setDimensionsRef(divElement); - }} - > - {children} -
-
+ <> + +
{ + setDimensionsRef(divElement); + }} + > + {children} +
+
+ + ); }; diff --git a/packages/kbn-grid-layout/grid/grid_overlay.tsx b/packages/kbn-grid-layout/grid/grid_overlay.tsx new file mode 100644 index 0000000000000..0f7d56792c5cc --- /dev/null +++ b/packages/kbn-grid-layout/grid/grid_overlay.tsx @@ -0,0 +1,146 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import { euiThemeVars } from '@kbn/ui-theme'; + +import React, { useEffect, useRef, useState } from 'react'; +import { GridLayoutStateManager } from './types'; + +type ScrollDirection = 'up' | 'down'; + +const scrollLabels: { [key in ScrollDirection]: string } = { + up: i18n.translate('kbnGridLayout.overlays.scrollUpLabel', { defaultMessage: 'Scroll up' }), + down: i18n.translate('kbnGridLayout.overlays.scrollDownLabel', { defaultMessage: 'Scroll down' }), +}; + +const scrollOnInterval = (direction: ScrollDirection) => { + const interval = setInterval(() => { + window.scroll({ + top: window.scrollY + (direction === 'down' ? 50 : -50), + behavior: 'smooth', + }); + }, 100); + return interval; +}; + +const ScrollOnHover = ({ + gridLayoutStateManager, + direction, +}: { + gridLayoutStateManager: GridLayoutStateManager; + direction: ScrollDirection; +}) => { + const buttonRef = useRef(null); + const containerRef = useRef(null); + + const [isHidden, setIsHidden] = useState(true); + const [isActive, setIsActive] = useState(false); + const scrollInterval = useRef(null); + + useEffect(() => { + gridLayoutStateManager.interactionEvent$.subscribe((interactionEvent) => { + if (interactionEvent) { + setIsHidden(false); + } else { + setIsHidden(true); + } + }); + }, [gridLayoutStateManager]); + + return ( +
{ + setIsActive(true); + scrollInterval.current = scrollOnInterval(direction); + }} + onMouseLeave={() => { + setIsActive(false); + if (scrollInterval.current) { + clearInterval(scrollInterval.current); + } + }} + className={'droppable'} + css={css` + pointer-events: ${isHidden ? 'none' : 'auto'}; + width: 100%; + position: fixed; + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + opacity: ${isHidden ? 0 : 1}; + background-color: transparent; + transition: opacity 100ms linear; + padding: ${euiThemeVars.euiSizeM}; + ${direction === 'down' ? 'bottom: 0;' : 'top: 0;'} + `} + > + {direction === 'up' && ( +
+ )} +
+ + {scrollLabels[direction]} + +
+
+ ); +}; + +export const GridOverlay = ({ + gridLayoutStateManager, +}: { + gridLayoutStateManager: GridLayoutStateManager; +}) => { + const ref = useRef(null); + + return ( + + + + + ); +}; diff --git a/packages/kbn-grid-layout/grid/grid_row.tsx b/packages/kbn-grid-layout/grid/grid_row.tsx index 32caf448926eb..9daa3a5f1325e 100644 --- a/packages/kbn-grid-layout/grid/grid_row.tsx +++ b/packages/kbn-grid-layout/grid/grid_row.tsx @@ -186,7 +186,7 @@ export const GridRow = forwardRef< }); } }, - [] + [gridLayoutStateManager.panelRefs, rowIndex, setInteractionEvent] ); /** @@ -209,7 +209,7 @@ export const GridRow = forwardRef< }} /> )); - }, [panelIds, rowIndex, gridLayoutStateManager, renderPanelContents, setInteractionEvent]); + }, [panelIds, rowIndex, gridLayoutStateManager, renderPanelContents, interactionStart]); return ( <>