interactionStart('resize', e)}
+ css={css`
+ right: 0;
+ bottom: 0;
+ opacity: 0;
+ margin: -2px;
+ position: absolute;
+ width: ${euiThemeVars.euiSizeL};
+ height: ${euiThemeVars.euiSizeL};
+ transition: opacity 0.2s, border 0.2s;
+ border-radius: 7px 0 7px 0;
+ border-bottom: 2px solid ${euiThemeVars.euiColorSuccess};
+ border-right: 2px solid ${euiThemeVars.euiColorSuccess};
+ :hover {
+ background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
+ cursor: se-resize;
+ }
+ `}
+ />
+
+ {renderPanelContents(panelData.id)}
+
+
+
+ );
+};
diff --git a/packages/kbn-grid-layout/grid/grid_row.tsx b/packages/kbn-grid-layout/grid/grid_row.tsx
new file mode 100644
index 0000000000000..3f2676a1db6ba
--- /dev/null
+++ b/packages/kbn-grid-layout/grid/grid_row.tsx
@@ -0,0 +1,124 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiButtonIcon, EuiFlexGroup, EuiSpacer, EuiTitle, transparentize } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { euiThemeVars } from '@kbn/ui-theme';
+import React, { forwardRef, useMemo } from 'react';
+import { GridPanel } from './grid_panel';
+import { GridRowData, PanelInteractionEvent, RuntimeGridSettings } from './types';
+
+const gridColor = transparentize(euiThemeVars.euiColorSuccess, 0.2);
+const getGridBackgroundCSS = (settings: RuntimeGridSettings) => {
+ const { gutterSize, columnPixelWidth, rowHeight } = settings;
+ return css`
+ background-position: top -${gutterSize / 2}px left -${gutterSize / 2}px;
+ background-size: ${columnPixelWidth + gutterSize}px ${rowHeight + gutterSize}px;
+ background-image: linear-gradient(to right, ${gridColor} 1px, transparent 1px),
+ linear-gradient(to bottom, ${gridColor} 1px, transparent 1px);
+ `;
+};
+
+export const GridRow = forwardRef<
+ HTMLDivElement,
+ {
+ rowIndex: number;
+ rowData: GridRowData;
+ toggleIsCollapsed: () => void;
+ activePanelId: string | undefined;
+ targetRowIndex: number | undefined;
+ runtimeSettings: RuntimeGridSettings;
+ renderPanelContents: (panelId: string) => React.ReactNode;
+ setInteractionEvent: (interactionData?: PanelInteractionEvent) => void;
+ }
+>(
+ (
+ {
+ rowData,
+ rowIndex,
+ activePanelId,
+ targetRowIndex,
+ runtimeSettings,
+ toggleIsCollapsed,
+ renderPanelContents,
+ setInteractionEvent,
+ },
+ gridRef
+ ) => {
+ const { gutterSize, columnCount, rowHeight } = runtimeSettings;
+ const isGridTargeted = activePanelId && targetRowIndex === rowIndex;
+
+ // calculate row count based on the number of rows needed to fit all panels
+ const rowCount = useMemo(() => {
+ const maxRow = Object.values(rowData.panels).reduce((acc, panel) => {
+ return Math.max(acc, panel.row + panel.height);
+ }, 0);
+ return maxRow || 1;
+ }, [rowData]);
+
+ return (
+ <>
+ {rowIndex !== 0 && (
+ <>
+
+ {Object.values(rowData.panels).map((panelData) => (
+ {
+ if (partialInteractionEvent) {
+ setInteractionEvent({
+ ...partialInteractionEvent,
+ targetRowIndex: rowIndex,
+ });
+ return;
+ }
+ setInteractionEvent();
+ }}
+ />
+ ))}
+
+ )}
+ >
+ );
+ }
+);
diff --git a/packages/kbn-grid-layout/grid/resolve_grid_row.ts b/packages/kbn-grid-layout/grid/resolve_grid_row.ts
new file mode 100644
index 0000000000000..1fb7d43dc35d2
--- /dev/null
+++ b/packages/kbn-grid-layout/grid/resolve_grid_row.ts
@@ -0,0 +1,107 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { GridPanelData, GridRowData } from './types';
+
+const collides = (panelA: GridPanelData, panelB: GridPanelData) => {
+ if (panelA.id === panelB.id) return false; // same panel
+ if (panelA.column + panelA.width <= panelB.column) return false; // panel a is left of panel b
+ if (panelA.column >= panelB.column + panelB.width) return false; // panel a is right of panel b
+ if (panelA.row + panelA.height <= panelB.row) return false; // panel a is above panel b
+ if (panelA.row >= panelB.row + panelB.height) return false; // panel a is below panel b
+ return true; // boxes overlap
+};
+
+const getAllCollisionsWithPanel = (
+ panelToCheck: GridPanelData,
+ gridLayout: GridRowData,
+ keysInOrder: string[]
+): GridPanelData[] => {
+ const collidingPanels: GridPanelData[] = [];
+ for (const key of keysInOrder) {
+ const comparePanel = gridLayout.panels[key];
+ if (comparePanel.id === panelToCheck.id) continue;
+ if (collides(panelToCheck, comparePanel)) {
+ collidingPanels.push(comparePanel);
+ }
+ }
+ return collidingPanels;
+};
+
+const getKeysInOrder = (rowData: GridRowData, draggedId?: string): string[] => {
+ const panelKeys = Object.keys(rowData.panels);
+ return panelKeys.sort((panelKeyA, panelKeyB) => {
+ const panelA = rowData.panels[panelKeyA];
+ const panelB = rowData.panels[panelKeyB];
+
+ // sort by row first
+ if (panelA.row > panelB.row) return 1;
+ if (panelA.row < panelB.row) return -1;
+
+ // if rows are the same. Is either panel being dragged?
+ if (panelA.id === draggedId) return -1;
+ if (panelB.id === draggedId) return 1;
+
+ // if rows are the same and neither panel is being dragged, sort by column
+ if (panelA.column > panelB.column) return 1;
+ if (panelA.column < panelB.column) return -1;
+
+ // fall back
+ return 1;
+ });
+};
+
+const compactGridRow = (originalLayout: GridRowData) => {
+ const nextRowData = { ...originalLayout, panels: { ...originalLayout.panels } };
+ // compact all vertical space.
+ const sortedKeysAfterMove = getKeysInOrder(nextRowData);
+ for (const panelKey of sortedKeysAfterMove) {
+ const panel = nextRowData.panels[panelKey];
+ // try moving panel up one row at a time until it collides
+ while (panel.row > 0) {
+ const collisions = getAllCollisionsWithPanel(
+ { ...panel, row: panel.row - 1 },
+ nextRowData,
+ sortedKeysAfterMove
+ );
+ if (collisions.length !== 0) break;
+ panel.row -= 1;
+ }
+ }
+ return nextRowData;
+};
+
+export const resolveGridRow = (
+ originalRowData: GridRowData,
+ dragRequest?: GridPanelData
+): GridRowData => {
+ const nextRowData = { ...originalRowData, panels: { ...originalRowData.panels } };
+
+ // Apply drag request
+ if (dragRequest) {
+ nextRowData.panels[dragRequest.id] = dragRequest;
+ }
+ // return nextRowData;
+
+ // push all panels down if they collide with another panel
+ const sortedKeys = getKeysInOrder(nextRowData, dragRequest?.id);
+
+ for (const key of sortedKeys) {
+ const panel = nextRowData.panels[key];
+ const collisions = getAllCollisionsWithPanel(panel, nextRowData, sortedKeys);
+
+ for (const collision of collisions) {
+ const rowOverlap = panel.row + panel.height - collision.row;
+ if (rowOverlap > 0) {
+ collision.row += rowOverlap;
+ }
+ }
+ }
+ const compactedGrid = compactGridRow(nextRowData);
+ return compactedGrid;
+};
diff --git a/packages/kbn-grid-layout/grid/types.ts b/packages/kbn-grid-layout/grid/types.ts
new file mode 100644
index 0000000000000..e3119f6e1cfd2
--- /dev/null
+++ b/packages/kbn-grid-layout/grid/types.ts
@@ -0,0 +1,97 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { BehaviorSubject } from 'rxjs';
+export interface GridCoordinate {
+ column: number;
+ row: number;
+}
+
+export interface GridRect extends GridCoordinate {
+ width: number;
+ height: number;
+}
+
+export interface GridPanelData extends GridRect {
+ id: string;
+}
+
+export interface GridRowData {
+ title: string;
+ isCollapsed: boolean;
+ panels: {
+ [key: string]: GridPanelData;
+ };
+}
+
+export type GridLayoutData = GridRowData[];
+
+export interface GridSettings {
+ gutterSize: number;
+ rowHeight: number;
+ columnCount: number;
+}
+
+/**
+ * The runtime settings for the grid, including the pixel width of each column
+ * which is calculated on the fly based on the grid settings and the width of
+ * the containing element.
+ */
+export type RuntimeGridSettings = GridSettings & { columnPixelWidth: number };
+
+export interface GridLayoutStateManager {
+ hideDragPreview: () => void;
+ updatePreviewElement: (rect: {
+ top: number;
+ left: number;
+ bottom: number;
+ right: number;
+ }) => void;
+
+ gridLayout$: BehaviorSubject