mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[kbn-grid-layout] Store rows in object instead of array (#212965)
Closes https://github.com/elastic/kibana/issues/211930 ## Summary This PR makes it so that `kbn-grid-layout` stores its rows as an object / dictionary (`{ [key: string]: GridRowData }`) rather than an array (`Array<GridRowData>`). This is a prerequisite for https://github.com/elastic/kibana/issues/190381 , since it allows us to re-order rows without re-rendering their contents. It also means that deleting a row will no longer cause the rows below it to re-render, since re-rendering is now dependant on the row's **ID** rather than the row's order. **Before** https://github.com/user-attachments/assets/83651b24-a32c-4953-8ad5-c0eced163eb5 **After** https://github.com/user-attachments/assets/9cef6dbc-3d62-46aa-bc40-ab24fc4e5556 ### Checklist - [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) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ef0c364f11
commit
b32f0fe1e8
29 changed files with 325 additions and 286 deletions
|
@ -8,9 +8,11 @@
|
|||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Subject, combineLatest, debounceTime, map, skip, take } from 'rxjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
EuiBadge,
|
||||
|
@ -88,8 +90,8 @@ export const GridExample = ({
|
|||
const currentPanel = panels[panelId];
|
||||
const savedPanel = savedState.current.panels[panelId];
|
||||
panelsAreEqual = deepEqual(
|
||||
{ row: 0, ...currentPanel.gridData },
|
||||
{ row: 0, ...savedPanel.gridData }
|
||||
{ row: 'first', ...currentPanel.gridData },
|
||||
{ row: 'first', ...savedPanel.gridData }
|
||||
);
|
||||
}
|
||||
const hasChanges = !(panelsAreEqual && deepEqual(rows, savedState.current.rows));
|
||||
|
@ -147,15 +149,17 @@ export const GridExample = ({
|
|||
);
|
||||
|
||||
const addNewSection = useCallback(() => {
|
||||
mockDashboardApi.rows$.next([
|
||||
...mockDashboardApi.rows$.getValue(),
|
||||
{
|
||||
title: i18n.translate('examples.gridExample.defaultSectionTitle', {
|
||||
defaultMessage: 'New collapsible section',
|
||||
}),
|
||||
collapsed: false,
|
||||
},
|
||||
]);
|
||||
const rows = cloneDeep(mockDashboardApi.rows$.getValue());
|
||||
const id = uuidv4();
|
||||
rows[id] = {
|
||||
id,
|
||||
order: Object.keys(rows).length,
|
||||
title: i18n.translate('examples.gridExample.defaultSectionTitle', {
|
||||
defaultMessage: 'New collapsible section',
|
||||
}),
|
||||
collapsed: false,
|
||||
};
|
||||
mockDashboardApi.rows$.next(rows);
|
||||
|
||||
// scroll to bottom after row is added
|
||||
layoutUpdated$.pipe(skip(1), take(1)).subscribe(() => {
|
||||
|
|
|
@ -1004,7 +1004,7 @@
|
|||
"w": 48,
|
||||
"h": 17,
|
||||
"i": "4",
|
||||
"row": 1
|
||||
"row": "second"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "4",
|
||||
|
@ -1035,7 +1035,7 @@
|
|||
"w": 18,
|
||||
"h": 8,
|
||||
"i": "05da0d2b-0145-4068-b21c-00be3184d465",
|
||||
"row": 1
|
||||
"row": "second"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "05da0d2b-0145-4068-b21c-00be3184d465",
|
||||
|
@ -1073,7 +1073,7 @@
|
|||
"w": 18,
|
||||
"h": 16,
|
||||
"i": "b7da9075-4742-47e3-b4f8-fc9ba82de74c",
|
||||
"row": 1
|
||||
"row": "second"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "b7da9075-4742-47e3-b4f8-fc9ba82de74c",
|
||||
|
@ -1111,7 +1111,7 @@
|
|||
"w": 12,
|
||||
"h": 16,
|
||||
"i": "5c409557-644d-4c05-a284-ffe54bb28db0",
|
||||
"row": 1
|
||||
"row": "second"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "5c409557-644d-4c05-a284-ffe54bb28db0",
|
||||
|
@ -1234,7 +1234,7 @@
|
|||
"w": 6,
|
||||
"h": 8,
|
||||
"i": "af4b5c07-506e-44c2-b2bb-2113d0c5b274",
|
||||
"row": 1
|
||||
"row": "second"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "af4b5c07-506e-44c2-b2bb-2113d0c5b274",
|
||||
|
@ -1400,7 +1400,7 @@
|
|||
"w": 6,
|
||||
"h": 8,
|
||||
"i": "d42c4870-c028-4d8a-abd0-0effbc190ce3",
|
||||
"row": 1
|
||||
"row": "second"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "d42c4870-c028-4d8a-abd0-0effbc190ce3",
|
||||
|
@ -1520,7 +1520,7 @@
|
|||
"w": 6,
|
||||
"h": 8,
|
||||
"i": "4092d42c-f93b-4c71-a6db-8f12abf12791",
|
||||
"row": 1
|
||||
"row": "second"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "4092d42c-f93b-4c71-a6db-8f12abf12791",
|
||||
|
@ -1641,7 +1641,7 @@
|
|||
"w": 30,
|
||||
"h": 15,
|
||||
"i": "15",
|
||||
"row": 2
|
||||
"row": "third"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "15",
|
||||
|
@ -1887,7 +1887,7 @@
|
|||
"w": 18,
|
||||
"h": 8,
|
||||
"i": "4e64d6d7-4f92-4d5e-abbb-13796604db30",
|
||||
"row": 2
|
||||
"row": "third"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "4e64d6d7-4f92-4d5e-abbb-13796604db30v",
|
||||
|
@ -1925,7 +1925,7 @@
|
|||
"w": 6,
|
||||
"h": 7,
|
||||
"i": "ddce4ad8-6a82-44f0-9995-57f46f153f50",
|
||||
"row": 2
|
||||
"row": "third"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "ddce4ad8-6a82-44f0-9995-57f46f153f50",
|
||||
|
@ -2120,7 +2120,7 @@
|
|||
"w": 6,
|
||||
"h": 7,
|
||||
"i": "a2884704-db3b-4b92-a19a-cdfe668dec39",
|
||||
"row": 2
|
||||
"row": "third"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "a2884704-db3b-4b92-a19a-cdfe668dec39",
|
||||
|
@ -2315,7 +2315,7 @@
|
|||
"w": 6,
|
||||
"h": 7,
|
||||
"i": "529eec49-10e2-4a40-9c77-5c81f4eb3943",
|
||||
"row": 2
|
||||
"row": "third"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "529eec49-10e2-4a40-9c77-5c81f4eb3943",
|
||||
|
@ -2510,7 +2510,7 @@
|
|||
"w": 48,
|
||||
"h": 12,
|
||||
"i": "1d5f0b3f-d9d2-4b26-997b-83bc5ca3090b",
|
||||
"row": 2
|
||||
"row": "third"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "1d5f0b3f-d9d2-4b26-997b-83bc5ca3090b",
|
||||
|
@ -2905,7 +2905,7 @@
|
|||
"w": 48,
|
||||
"h": 15,
|
||||
"i": "9f79ecca-123f-4098-a658-6b0e998da003",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "9f79ecca-123f-4098-a658-6b0e998da003",
|
||||
|
@ -2922,7 +2922,7 @@
|
|||
"w": 24,
|
||||
"h": 9,
|
||||
"i": "7",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "7",
|
||||
|
@ -3161,7 +3161,7 @@
|
|||
"w": 24,
|
||||
"h": 11,
|
||||
"i": "10",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "10",
|
||||
|
@ -3346,7 +3346,7 @@
|
|||
"w": 24,
|
||||
"h": 22,
|
||||
"i": "23",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "23",
|
||||
|
@ -3371,7 +3371,7 @@
|
|||
"w": 24,
|
||||
"h": 22,
|
||||
"i": "31",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "31",
|
||||
|
@ -3388,7 +3388,7 @@
|
|||
"w": 24,
|
||||
"h": 8,
|
||||
"i": "6afc61f7-e2d5-45a3-9e7a-281160ad3eb9",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "6afc61f7-e2d5-45a3-9e7a-281160ad3eb9",
|
||||
|
@ -3420,7 +3420,7 @@
|
|||
"w": 8,
|
||||
"h": 8,
|
||||
"i": "392b4936-f753-47bc-a98d-a4e41a0a4cd4",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "392b4936-f753-47bc-a98d-a4e41a0a4cd4",
|
||||
|
@ -3485,7 +3485,7 @@
|
|||
"w": 8,
|
||||
"h": 4,
|
||||
"i": "9271deff-5a61-4665-83fc-f9fdc6bf0c0b",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "9271deff-5a61-4665-83fc-f9fdc6bf0c0b",
|
||||
|
@ -3613,7 +3613,7 @@
|
|||
"w": 8,
|
||||
"h": 4,
|
||||
"i": "aa591c29-1a31-4ee1-a71d-b829c06fd162",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "aa591c29-1a31-4ee1-a71d-b829c06fd162",
|
||||
|
@ -3777,7 +3777,7 @@
|
|||
"w": 8,
|
||||
"h": 4,
|
||||
"i": "b766e3b8-4544-46ed-99e6-9ecc4847e2a2",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "b766e3b8-4544-46ed-99e6-9ecc4847e2a2",
|
||||
|
@ -3905,7 +3905,7 @@
|
|||
"w": 8,
|
||||
"h": 4,
|
||||
"i": "2e33ade5-96e5-40b4-b460-493e5d4fa834",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "2e33ade5-96e5-40b4-b460-493e5d4fa834",
|
||||
|
@ -4069,7 +4069,7 @@
|
|||
"w": 24,
|
||||
"h": 8,
|
||||
"i": "086ac2e9-dd16-4b45-92b8-1e43ff7e3f65",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "086ac2e9-dd16-4b45-92b8-1e43ff7e3f65",
|
||||
|
@ -4190,7 +4190,7 @@
|
|||
"w": 24,
|
||||
"h": 28,
|
||||
"i": "fb86b32f-fb7a-45cf-9511-f366fef51bbd",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "fb86b32f-fb7a-45cf-9511-f366fef51bbd",
|
||||
|
@ -4500,7 +4500,7 @@
|
|||
"w": 24,
|
||||
"h": 11,
|
||||
"i": "0cc42484-16f7-42ec-b38c-9bf8be69cde7",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "0cc42484-16f7-42ec-b38c-9bf8be69cde7",
|
||||
|
@ -4643,7 +4643,7 @@
|
|||
"w": 12,
|
||||
"h": 11,
|
||||
"i": "5d53db36-2d5a-4adc-af7b-cec4c1a294e0",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "5d53db36-2d5a-4adc-af7b-cec4c1a294e0",
|
||||
|
@ -4773,7 +4773,7 @@
|
|||
"w": 12,
|
||||
"h": 11,
|
||||
"i": "ecd89a7c-9124-4472-bdc6-9bdbd70d45d5",
|
||||
"row": 3
|
||||
"row": "fourth"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "ecd89a7c-9124-4472-bdc6-9bdbd70d45d5",
|
||||
|
|
|
@ -28,10 +28,10 @@ export function setSerializedGridLayout(state: MockSerializedDashboardState) {
|
|||
|
||||
const initialState: MockSerializedDashboardState = {
|
||||
panels: logsPanels,
|
||||
rows: [
|
||||
{ title: 'Request Sizes', collapsed: false },
|
||||
{ title: 'Visitors', collapsed: false },
|
||||
{ title: 'Response Codes', collapsed: false },
|
||||
{ title: 'Entire Flights Dashboard', collapsed: true },
|
||||
],
|
||||
rows: {
|
||||
first: { id: 'first', order: 0, title: 'Request Sizes', collapsed: false },
|
||||
second: { id: 'second', order: 1, title: 'Visitors', collapsed: false },
|
||||
third: { id: 'third', order: 2, title: 'Response Codes', collapsed: false },
|
||||
fourth: { id: 'fourth', order: 3, title: 'Entire Flights Dashboard', collapsed: true },
|
||||
},
|
||||
};
|
||||
|
|
|
@ -26,7 +26,7 @@ export interface DashboardGridData {
|
|||
|
||||
interface DashboardPanelState {
|
||||
type: string;
|
||||
gridData: DashboardGridData & { row?: number };
|
||||
gridData: DashboardGridData & { row?: string };
|
||||
explicitInput: Partial<any> & { id: string };
|
||||
version?: string;
|
||||
}
|
||||
|
@ -35,7 +35,9 @@ export interface MockedDashboardPanelMap {
|
|||
[key: string]: DashboardPanelState;
|
||||
}
|
||||
|
||||
export type MockedDashboardRowMap = Array<{ title: string; collapsed: boolean }>;
|
||||
export interface MockedDashboardRowMap {
|
||||
[id: string]: { id: string; order: number; title: string; collapsed: boolean };
|
||||
}
|
||||
|
||||
export interface MockSerializedDashboardState {
|
||||
panels: MockedDashboardPanelMap;
|
||||
|
|
|
@ -99,7 +99,7 @@ export const useMockDashboardApi = ({
|
|||
[newId]: {
|
||||
type: panelPackage.panelType,
|
||||
gridData: {
|
||||
row: 0,
|
||||
row: 'first',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: DEFAULT_PANEL_WIDTH,
|
||||
|
|
|
@ -15,10 +15,11 @@ export const gridLayoutToDashboardPanelMap = (
|
|||
layout: GridLayoutData
|
||||
): { panels: MockedDashboardPanelMap; rows: MockedDashboardRowMap } => {
|
||||
const panels: MockedDashboardPanelMap = {};
|
||||
const rows: MockedDashboardRowMap = [];
|
||||
layout.forEach((row, rowIndex) => {
|
||||
rows.push({ title: row.title, collapsed: row.isCollapsed });
|
||||
Object.values(row.panels).forEach((panelGridData) => {
|
||||
const rows: MockedDashboardRowMap = {};
|
||||
Object.entries(layout).forEach(([rowId, row]) => {
|
||||
const { panels: rowPanels, isCollapsed, ...rest } = row; // drop panels
|
||||
rows[rowId] = { ...rest, collapsed: isCollapsed };
|
||||
Object.values(rowPanels).forEach((panelGridData) => {
|
||||
panels[panelGridData.id] = {
|
||||
...panelState[panelGridData.id],
|
||||
gridData: {
|
||||
|
@ -27,7 +28,7 @@ export const gridLayoutToDashboardPanelMap = (
|
|||
x: panelGridData.column,
|
||||
w: panelGridData.width,
|
||||
h: panelGridData.height,
|
||||
row: rowIndex,
|
||||
row: rowId,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -42,15 +43,19 @@ export const dashboardInputToGridLayout = ({
|
|||
panels: MockedDashboardPanelMap;
|
||||
rows: MockedDashboardRowMap;
|
||||
}): GridLayoutData => {
|
||||
const layout: GridLayoutData = [];
|
||||
|
||||
rows.forEach((row) => {
|
||||
layout.push({ title: row.title, isCollapsed: row.collapsed, panels: {} });
|
||||
const layout: GridLayoutData = {};
|
||||
Object.values(rows).forEach((row) => {
|
||||
const { collapsed, ...rest } = row;
|
||||
layout[row.id] = {
|
||||
...rest,
|
||||
panels: {},
|
||||
isCollapsed: collapsed,
|
||||
};
|
||||
});
|
||||
|
||||
Object.keys(panels).forEach((panelId) => {
|
||||
const gridData = panels[panelId].gridData;
|
||||
layout[gridData.row ?? 0].panels[panelId] = {
|
||||
layout[gridData.row ?? 'first'].panels[panelId] = {
|
||||
id: panelId,
|
||||
row: gridData.y,
|
||||
column: gridData.x,
|
||||
|
|
|
@ -13,7 +13,7 @@ import { combineLatest, skip } from 'rxjs';
|
|||
import { css } from '@emotion/react';
|
||||
import { useGridLayoutContext } from './use_grid_layout_context';
|
||||
|
||||
export const DragPreview = React.memo(({ rowIndex }: { rowIndex: number }) => {
|
||||
export const DragPreview = React.memo(({ rowId }: { rowId: string }) => {
|
||||
const { gridLayoutStateManager } = useGridLayoutContext();
|
||||
|
||||
const dragPreviewRef = useRef<HTMLDivElement | null>(null);
|
||||
|
@ -29,10 +29,10 @@ export const DragPreview = React.memo(({ rowIndex }: { rowIndex: number }) => {
|
|||
.subscribe(([activePanel, proposedGridLayout]) => {
|
||||
if (!dragPreviewRef.current) return;
|
||||
|
||||
if (!activePanel || !proposedGridLayout?.[rowIndex].panels[activePanel.id]) {
|
||||
if (!activePanel || !proposedGridLayout?.[rowId].panels[activePanel.id]) {
|
||||
dragPreviewRef.current.style.display = 'none';
|
||||
} else {
|
||||
const panel = proposedGridLayout[rowIndex].panels[activePanel.id];
|
||||
const panel = proposedGridLayout[rowId].panels[activePanel.id];
|
||||
dragPreviewRef.current.style.display = 'block';
|
||||
dragPreviewRef.current.style.gridColumnStart = `${panel.column + 1}`;
|
||||
dragPreviewRef.current.style.gridColumnEnd = `${panel.column + 1 + panel.width}`;
|
||||
|
|
|
@ -96,10 +96,10 @@ describe('GridLayout', () => {
|
|||
|
||||
// if layout **has** changed, call `onLayoutChange`
|
||||
const newLayout = cloneDeep(layout);
|
||||
newLayout[0] = {
|
||||
...newLayout[0],
|
||||
newLayout.first = {
|
||||
...newLayout.first,
|
||||
panels: {
|
||||
...newLayout[0].panels,
|
||||
...newLayout.first.panels,
|
||||
panel1: {
|
||||
id: 'panel1',
|
||||
row: 100,
|
||||
|
@ -217,7 +217,7 @@ describe('GridLayout', () => {
|
|||
it('after removing a panel', async () => {
|
||||
const { rerender } = renderGridLayout();
|
||||
const sampleLayoutWithoutPanel1 = cloneDeep(getSampleLayout());
|
||||
delete sampleLayoutWithoutPanel1[0].panels.panel1;
|
||||
delete sampleLayoutWithoutPanel1.first.panels.panel1;
|
||||
rerender({ layout: sampleLayoutWithoutPanel1 });
|
||||
|
||||
expect(getAllThePanelIds()).toEqual([
|
||||
|
@ -236,9 +236,9 @@ describe('GridLayout', () => {
|
|||
it('after replacing a panel id', async () => {
|
||||
const { rerender } = renderGridLayout();
|
||||
const modifiedLayout = cloneDeep(getSampleLayout());
|
||||
const newPanel = { ...modifiedLayout[0].panels.panel1, id: 'panel11' };
|
||||
delete modifiedLayout[0].panels.panel1;
|
||||
modifiedLayout[0].panels.panel11 = newPanel;
|
||||
const newPanel = { ...modifiedLayout.first.panels.panel1, id: 'panel11' };
|
||||
delete modifiedLayout.first.panels.panel1;
|
||||
modifiedLayout.first.panels.panel11 = newPanel;
|
||||
|
||||
rerender({ layout: modifiedLayout });
|
||||
|
||||
|
|
|
@ -8,9 +8,10 @@
|
|||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { combineLatest, distinctUntilChanged, map, pairwise, skip } from 'rxjs';
|
||||
import { combineLatest, pairwise } from 'rxjs';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
|
@ -20,7 +21,7 @@ import { GridAccessMode, GridLayoutData, GridSettings, UseCustomDragHandle } fro
|
|||
import { GridLayoutContext, GridLayoutContextType } from './use_grid_layout_context';
|
||||
import { useGridLayoutState } from './use_grid_layout_state';
|
||||
import { isLayoutEqual } from './utils/equality_checks';
|
||||
import { resolveGridRow } from './utils/resolve_grid_row';
|
||||
import { getRowKeysInOrder, resolveGridRow } from './utils/resolve_grid_row';
|
||||
|
||||
export type GridLayoutProps = {
|
||||
layout: GridLayoutData;
|
||||
|
@ -50,10 +51,7 @@ export const GridLayout = ({
|
|||
accessMode,
|
||||
});
|
||||
|
||||
const [rowCount, setRowCount] = useState<number>(
|
||||
gridLayoutStateManager.gridLayout$.getValue().length
|
||||
);
|
||||
|
||||
const [rowIdsInOrder, setRowIdsInOrder] = useState<string[]>(getRowKeysInOrder(layout));
|
||||
/**
|
||||
* Update the `gridLayout$` behaviour subject in response to the `layout` prop changing
|
||||
*/
|
||||
|
@ -64,8 +62,8 @@ export const GridLayout = ({
|
|||
* the layout sent in as a prop is not guaranteed to be valid (i.e it may have floating panels) -
|
||||
* so, we need to loop through each row and ensure it is compacted
|
||||
*/
|
||||
newLayout.forEach((row, rowIndex) => {
|
||||
newLayout[rowIndex] = resolveGridRow(row);
|
||||
Object.entries(newLayout).forEach(([rowId, row]) => {
|
||||
newLayout[rowId] = resolveGridRow(row);
|
||||
});
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}
|
||||
|
@ -77,27 +75,18 @@ export const GridLayout = ({
|
|||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* The only thing that should cause the entire layout to re-render is adding a new row;
|
||||
* this subscription ensures this by updating the `rowCount` state when it changes.
|
||||
*/
|
||||
const rowCountSubscription = gridLayoutStateManager.gridLayout$
|
||||
.pipe(
|
||||
skip(1), // we initialized `rowCount` above, so skip the initial emit
|
||||
map((newLayout) => newLayout.length),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe((newRowCount) => {
|
||||
setRowCount(newRowCount);
|
||||
});
|
||||
|
||||
/**
|
||||
* This subscription calls the passed `onLayoutChange` callback when the layout changes
|
||||
* This subscription calls the passed `onLayoutChange` callback when the layout changes;
|
||||
* if the row IDs have changed, it also sets `rowIdsInOrder` to trigger a re-render
|
||||
*/
|
||||
const onLayoutChangeSubscription = gridLayoutStateManager.gridLayout$
|
||||
.pipe(pairwise())
|
||||
.subscribe(([layoutBefore, layoutAfter]) => {
|
||||
if (!isLayoutEqual(layoutBefore, layoutAfter)) {
|
||||
onLayoutChange(layoutAfter);
|
||||
|
||||
if (!deepEqual(Object.keys(layoutBefore), Object.keys(layoutAfter))) {
|
||||
setRowIdsInOrder(getRowKeysInOrder(layoutAfter));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -125,7 +114,6 @@ export const GridLayout = ({
|
|||
});
|
||||
|
||||
return () => {
|
||||
rowCountSubscription.unsubscribe();
|
||||
onLayoutChangeSubscription.unsubscribe();
|
||||
gridLayoutClassSubscription.unsubscribe();
|
||||
};
|
||||
|
@ -158,9 +146,9 @@ export const GridLayout = ({
|
|||
styles.hasExpandedPanel,
|
||||
]}
|
||||
>
|
||||
{Array.from({ length: rowCount }, (_, rowIndex) => {
|
||||
return <GridRow key={rowIndex} rowIndex={rowIndex} />;
|
||||
})}
|
||||
{rowIdsInOrder.map((rowId) => (
|
||||
<GridRow key={rowId} rowId={rowId} />
|
||||
))}
|
||||
</div>
|
||||
</GridHeightSmoother>
|
||||
</GridLayoutContext.Provider>
|
||||
|
|
|
@ -20,17 +20,17 @@ export interface DragHandleApi {
|
|||
|
||||
export const useDragHandleApi = ({
|
||||
panelId,
|
||||
rowIndex,
|
||||
rowId,
|
||||
}: {
|
||||
panelId: string;
|
||||
rowIndex: number;
|
||||
rowId: string;
|
||||
}): DragHandleApi => {
|
||||
const { useCustomDragHandle } = useGridLayoutContext();
|
||||
|
||||
const startInteraction = useGridLayoutEvents({
|
||||
interactionType: 'drag',
|
||||
panelId,
|
||||
rowIndex,
|
||||
rowId,
|
||||
});
|
||||
|
||||
const removeEventListenersRef = useRef<(() => void) | null>(null);
|
||||
|
|
|
@ -25,7 +25,7 @@ describe('GridPanel', () => {
|
|||
} as GridLayoutContextType;
|
||||
const panelProps = {
|
||||
panelId: 'panel1',
|
||||
rowIndex: 0,
|
||||
rowId: 'first',
|
||||
...(overrides?.propsOverrides ?? {}),
|
||||
};
|
||||
const { rerender, ...rtlRest } = render(
|
||||
|
|
|
@ -20,20 +20,20 @@ import { ResizeHandle } from './resize_handle';
|
|||
|
||||
export interface GridPanelProps {
|
||||
panelId: string;
|
||||
rowIndex: number;
|
||||
rowId: string;
|
||||
}
|
||||
|
||||
export const GridPanel = React.memo(({ panelId, rowIndex }: GridPanelProps) => {
|
||||
export const GridPanel = React.memo(({ panelId, rowId }: GridPanelProps) => {
|
||||
const { gridLayoutStateManager, useCustomDragHandle, renderPanelContents } =
|
||||
useGridLayoutContext();
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const dragHandleApi = useDragHandleApi({ panelId, rowIndex });
|
||||
const dragHandleApi = useDragHandleApi({ panelId, rowId });
|
||||
|
||||
/** Set initial styles based on state at mount to prevent styles from "blipping" */
|
||||
const initialStyles = useMemo(() => {
|
||||
const initialPanel = (gridLayoutStateManager.proposedGridLayout$.getValue() ??
|
||||
gridLayoutStateManager.gridLayout$.getValue())[rowIndex].panels[panelId];
|
||||
gridLayoutStateManager.gridLayout$.getValue())[rowId].panels[panelId];
|
||||
return css`
|
||||
position: relative;
|
||||
height: calc(
|
||||
|
@ -48,7 +48,7 @@ export const GridPanel = React.memo(({ panelId, rowIndex }: GridPanelProps) => {
|
|||
grid-row-start: ${initialPanel.row + 1};
|
||||
grid-row-end: ${initialPanel.row + 1 + initialPanel.height};
|
||||
`;
|
||||
}, [gridLayoutStateManager, rowIndex, panelId]);
|
||||
}, [gridLayoutStateManager, rowId, panelId]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
|
@ -60,8 +60,8 @@ export const GridPanel = React.memo(({ panelId, rowIndex }: GridPanelProps) => {
|
|||
])
|
||||
.pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
|
||||
.subscribe(([activePanel, gridLayout, proposedGridLayout]) => {
|
||||
const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
const panel = (proposedGridLayout ?? gridLayout)[rowIndex].panels[panelId];
|
||||
const ref = gridLayoutStateManager.panelRefs.current[rowId][panelId];
|
||||
const panel = (proposedGridLayout ?? gridLayout)[rowId]?.panels[panelId];
|
||||
if (!ref || !panel) return;
|
||||
|
||||
const currentInteractionEvent = gridLayoutStateManager.interactionEvent$.getValue();
|
||||
|
@ -128,9 +128,9 @@ export const GridPanel = React.memo(({ panelId, rowIndex }: GridPanelProps) => {
|
|||
*/
|
||||
const expandedPanelSubscription = gridLayoutStateManager.expandedPanelId$.subscribe(
|
||||
(expandedPanelId) => {
|
||||
const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
const ref = gridLayoutStateManager.panelRefs.current[rowId][panelId];
|
||||
const gridLayout = gridLayoutStateManager.gridLayout$.getValue();
|
||||
const panel = gridLayout[rowIndex].panels[panelId];
|
||||
const panel = gridLayout[rowId].panels[panelId];
|
||||
if (!ref || !panel) return;
|
||||
|
||||
if (expandedPanelId && expandedPanelId === panelId) {
|
||||
|
@ -160,17 +160,17 @@ export const GridPanel = React.memo(({ panelId, rowIndex }: GridPanelProps) => {
|
|||
return (
|
||||
<div
|
||||
ref={(element) => {
|
||||
if (!gridLayoutStateManager.panelRefs.current[rowIndex]) {
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex] = {};
|
||||
if (!gridLayoutStateManager.panelRefs.current[rowId]) {
|
||||
gridLayoutStateManager.panelRefs.current[rowId] = {};
|
||||
}
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex][panelId] = element;
|
||||
gridLayoutStateManager.panelRefs.current[rowId][panelId] = element;
|
||||
}}
|
||||
css={initialStyles}
|
||||
className="kbnGridPanel"
|
||||
>
|
||||
{!useCustomDragHandle && <DefaultDragHandle dragHandleApi={dragHandleApi} />}
|
||||
{panelContents}
|
||||
<ResizeHandle panelId={panelId} rowIndex={rowIndex} />
|
||||
<ResizeHandle panelId={panelId} rowId={rowId} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -15,27 +15,25 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { useGridLayoutEvents } from '../use_grid_layout_events';
|
||||
|
||||
export const ResizeHandle = React.memo(
|
||||
({ rowIndex, panelId }: { rowIndex: number; panelId: string }) => {
|
||||
const startInteraction = useGridLayoutEvents({
|
||||
interactionType: 'resize',
|
||||
panelId,
|
||||
rowIndex,
|
||||
});
|
||||
export const ResizeHandle = React.memo(({ rowId, panelId }: { rowId: string; panelId: string }) => {
|
||||
const startInteraction = useGridLayoutEvents({
|
||||
interactionType: 'resize',
|
||||
panelId,
|
||||
rowId,
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
css={styles}
|
||||
onMouseDown={startInteraction}
|
||||
onTouchStart={startInteraction}
|
||||
className="kbnGridPanel--resizeHandle"
|
||||
aria-label={i18n.translate('kbnGridLayout.resizeHandle.ariaLabel', {
|
||||
defaultMessage: 'Resize panel',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<button
|
||||
css={styles}
|
||||
onMouseDown={startInteraction}
|
||||
onTouchStart={startInteraction}
|
||||
className="kbnGridPanel--resizeHandle"
|
||||
aria-label={i18n.translate('kbnGridLayout.resizeHandle.ariaLabel', {
|
||||
defaultMessage: 'Resize panel',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
|
|
|
@ -23,10 +23,10 @@ import { deleteRow, movePanelsToRow } from '../utils/row_management';
|
|||
import { useGridLayoutContext } from '../use_grid_layout_context';
|
||||
|
||||
export const DeleteGridRowModal = ({
|
||||
rowIndex,
|
||||
rowId,
|
||||
setDeleteModalVisible,
|
||||
}: {
|
||||
rowIndex: number;
|
||||
rowId: string;
|
||||
setDeleteModalVisible: (visible: boolean) => void;
|
||||
}) => {
|
||||
const { gridLayoutStateManager } = useGridLayoutContext();
|
||||
|
@ -63,12 +63,11 @@ export const DeleteGridRowModal = ({
|
|||
<EuiButton
|
||||
onClick={() => {
|
||||
setDeleteModalVisible(false);
|
||||
let newLayout = movePanelsToRow(
|
||||
gridLayoutStateManager.gridLayout$.getValue(),
|
||||
rowIndex,
|
||||
0
|
||||
);
|
||||
newLayout = deleteRow(newLayout, rowIndex);
|
||||
const layout = gridLayoutStateManager.gridLayout$.getValue();
|
||||
const firstRowId = Object.values(layout).find(({ order }) => order === 0)?.id;
|
||||
if (!firstRowId) return;
|
||||
let newLayout = movePanelsToRow(layout, rowId, firstRowId);
|
||||
newLayout = deleteRow(newLayout, rowId);
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}}
|
||||
color="danger"
|
||||
|
@ -80,7 +79,7 @@ export const DeleteGridRowModal = ({
|
|||
<EuiButton
|
||||
onClick={() => {
|
||||
setDeleteModalVisible(false);
|
||||
const newLayout = deleteRow(gridLayoutStateManager.gridLayout$.getValue(), rowIndex);
|
||||
const newLayout = deleteRow(gridLayoutStateManager.gridLayout$.getValue(), rowId);
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}}
|
||||
fill
|
||||
|
@ -90,9 +89,8 @@ export const DeleteGridRowModal = ({
|
|||
defaultMessage:
|
||||
'Delete section and {panelCount} {panelCount, plural, one {panel} other {panels}}',
|
||||
values: {
|
||||
panelCount: Object.keys(
|
||||
gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels
|
||||
).length,
|
||||
panelCount: Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowId].panels)
|
||||
.length,
|
||||
},
|
||||
})}
|
||||
</EuiButton>
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('GridRow', () => {
|
|||
} as GridLayoutContextType
|
||||
}
|
||||
>
|
||||
<GridRow rowIndex={0} {...propsOverrides} />
|
||||
<GridRow rowId={'first'} {...propsOverrides} />
|
||||
</GridLayoutContext.Provider>,
|
||||
{ wrapper: EuiThemeProvider }
|
||||
);
|
||||
|
@ -40,22 +40,22 @@ describe('GridRow', () => {
|
|||
|
||||
it('renders all the panels in a row', () => {
|
||||
renderGridRow();
|
||||
const firstRowPanels = Object.values(getSampleLayout()[0].panels);
|
||||
const firstRowPanels = Object.values(getSampleLayout().first.panels);
|
||||
firstRowPanels.forEach((panel) => {
|
||||
expect(screen.getByLabelText(`panelId:${panel.id}`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show the panels in a row that is collapsed', async () => {
|
||||
renderGridRow({ rowIndex: 1 });
|
||||
renderGridRow({ rowId: 'second' });
|
||||
|
||||
expect(screen.getByTestId('kbnGridRowTitle-1').ariaExpanded).toBe('true');
|
||||
expect(screen.getByTestId('kbnGridRowTitle-second').ariaExpanded).toBe('true');
|
||||
expect(screen.getAllByText(/panel content/)).toHaveLength(1);
|
||||
|
||||
const collapseButton = screen.getByRole('button', { name: /toggle collapse/i });
|
||||
await userEvent.click(collapseButton);
|
||||
|
||||
expect(screen.getByTestId('kbnGridRowTitle-1').ariaExpanded).toBe('false');
|
||||
expect(screen.getByTestId('kbnGridRowTitle-second').ariaExpanded).toBe('false');
|
||||
expect(screen.queryAllByText(/panel content/)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,20 +17,20 @@ import { css } from '@emotion/react';
|
|||
import { DragPreview } from '../drag_preview';
|
||||
import { GridPanel } from '../grid_panel';
|
||||
import { useGridLayoutContext } from '../use_grid_layout_context';
|
||||
import { getKeysInOrder } from '../utils/resolve_grid_row';
|
||||
import { getPanelKeysInOrder } from '../utils/resolve_grid_row';
|
||||
import { GridRowHeader } from './grid_row_header';
|
||||
|
||||
export interface GridRowProps {
|
||||
rowIndex: number;
|
||||
rowId: string;
|
||||
}
|
||||
|
||||
export const GridRow = React.memo(({ rowIndex }: GridRowProps) => {
|
||||
export const GridRow = React.memo(({ rowId }: GridRowProps) => {
|
||||
const { gridLayoutStateManager } = useGridLayoutContext();
|
||||
const collapseButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex];
|
||||
const currentRow = gridLayoutStateManager.gridLayout$.value[rowId];
|
||||
|
||||
const [panelIdsInOrder, setPanelIdsInOrder] = useState<string[]>(() =>
|
||||
getKeysInOrder(currentRow.panels)
|
||||
getPanelKeysInOrder(currentRow.panels)
|
||||
);
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(currentRow.isCollapsed);
|
||||
|
||||
|
@ -40,10 +40,10 @@ export const GridRow = React.memo(({ rowIndex }: GridRowProps) => {
|
|||
const interactionStyleSubscription = gridLayoutStateManager.interactionEvent$
|
||||
.pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
|
||||
.subscribe((interactionEvent) => {
|
||||
const rowRef = gridLayoutStateManager.rowRefs.current[rowIndex];
|
||||
const rowRef = gridLayoutStateManager.rowRefs.current[rowId];
|
||||
if (!rowRef) return;
|
||||
const targetRow = interactionEvent?.targetRowIndex;
|
||||
if (rowIndex === targetRow && interactionEvent) {
|
||||
const targetRow = interactionEvent?.targetRow;
|
||||
if (rowId === targetRow && interactionEvent) {
|
||||
rowRef.classList.add('kbnGridRow--targeted');
|
||||
} else {
|
||||
rowRef.classList.remove('kbnGridRow--targeted');
|
||||
|
@ -63,8 +63,8 @@ export const GridRow = React.memo(({ rowIndex }: GridRowProps) => {
|
|||
map(([proposedGridLayout, gridLayout]) => {
|
||||
const displayedGridLayout = proposedGridLayout ?? gridLayout;
|
||||
return {
|
||||
isCollapsed: displayedGridLayout[rowIndex]?.isCollapsed ?? false,
|
||||
panelIds: Object.keys(displayedGridLayout[rowIndex]?.panels ?? {}),
|
||||
isCollapsed: displayedGridLayout[rowId]?.isCollapsed ?? false,
|
||||
panelIds: Object.keys(displayedGridLayout[rowId]?.panels ?? {}),
|
||||
};
|
||||
}),
|
||||
pairwise()
|
||||
|
@ -81,9 +81,9 @@ export const GridRow = React.memo(({ rowIndex }: GridRowProps) => {
|
|||
)
|
||||
) {
|
||||
setPanelIdsInOrder(
|
||||
getKeysInOrder(
|
||||
getPanelKeysInOrder(
|
||||
(gridLayoutStateManager.proposedGridLayout$.getValue() ??
|
||||
gridLayoutStateManager.gridLayout$.getValue())[rowIndex]?.panels ?? {}
|
||||
gridLayoutStateManager.gridLayout$.getValue())[rowId]?.panels ?? {}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -95,8 +95,8 @@ export const GridRow = React.memo(({ rowIndex }: GridRowProps) => {
|
|||
* reasons (screen readers and focus management).
|
||||
*/
|
||||
const gridLayoutSubscription = gridLayoutStateManager.gridLayout$.subscribe((gridLayout) => {
|
||||
if (!gridLayout[rowIndex]) return;
|
||||
const newPanelIdsInOrder = getKeysInOrder(gridLayout[rowIndex].panels);
|
||||
if (!gridLayout[rowId]) return;
|
||||
const newPanelIdsInOrder = getPanelKeysInOrder(gridLayout[rowId].panels);
|
||||
if (panelIdsInOrder.join() !== newPanelIdsInOrder.join()) {
|
||||
setPanelIdsInOrder(newPanelIdsInOrder);
|
||||
}
|
||||
|
@ -109,14 +109,14 @@ export const GridRow = React.memo(({ rowIndex }: GridRowProps) => {
|
|||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[rowIndex]
|
||||
[rowId]
|
||||
);
|
||||
|
||||
const toggleIsCollapsed = useCallback(() => {
|
||||
const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value);
|
||||
newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed;
|
||||
newLayout[rowId].isCollapsed = !newLayout[rowId].isCollapsed;
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}, [rowIndex, gridLayoutStateManager.gridLayout$]);
|
||||
}, [rowId, gridLayoutStateManager.gridLayout$]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
|
@ -134,29 +134,29 @@ export const GridRow = React.memo(({ rowIndex }: GridRowProps) => {
|
|||
'kbnGridRowContainer--collapsed': isCollapsed,
|
||||
})}
|
||||
>
|
||||
{rowIndex !== 0 && (
|
||||
{currentRow.order !== 0 && (
|
||||
<GridRowHeader
|
||||
rowIndex={rowIndex}
|
||||
rowId={rowId}
|
||||
toggleIsCollapsed={toggleIsCollapsed}
|
||||
collapseButtonRef={collapseButtonRef}
|
||||
/>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<div
|
||||
id={`kbnGridRow-${rowIndex}`}
|
||||
id={`kbnGridRow-${rowId}`}
|
||||
className={'kbnGridRow'}
|
||||
ref={(element: HTMLDivElement | null) =>
|
||||
(gridLayoutStateManager.rowRefs.current[rowIndex] = element)
|
||||
(gridLayoutStateManager.rowRefs.current[rowId] = element)
|
||||
}
|
||||
css={[styles.fullHeight, styles.grid]}
|
||||
role="region"
|
||||
aria-labelledby={`kbnGridRowTile-${rowIndex}`}
|
||||
aria-labelledby={`kbnGridRowTile-${rowId}`}
|
||||
>
|
||||
{/* render the panels **in order** for accessibility, using the memoized panel components */}
|
||||
{panelIdsInOrder.map((panelId) => (
|
||||
<GridPanel key={panelId} panelId={panelId} rowIndex={rowIndex} />
|
||||
<GridPanel key={panelId} panelId={panelId} rowId={rowId} />
|
||||
))}
|
||||
<DragPreview rowIndex={rowIndex} />
|
||||
<DragPreview rowId={rowId} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -20,9 +20,9 @@ import { GridLayoutContext, GridLayoutContextType } from '../use_grid_layout_con
|
|||
|
||||
const toggleIsCollapsed = jest
|
||||
.fn()
|
||||
.mockImplementation((rowIndex: number, gridLayoutStateManager: GridLayoutStateManager) => {
|
||||
.mockImplementation((rowId: string, gridLayoutStateManager: GridLayoutStateManager) => {
|
||||
const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value);
|
||||
newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed;
|
||||
newLayout[rowId].isCollapsed = !newLayout[rowId].isCollapsed;
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
});
|
||||
|
||||
|
@ -44,8 +44,8 @@ describe('GridRowHeader', () => {
|
|||
}
|
||||
>
|
||||
<GridRowHeader
|
||||
rowIndex={0}
|
||||
toggleIsCollapsed={() => toggleIsCollapsed(0, stateManagerMock)}
|
||||
rowId={'first'}
|
||||
toggleIsCollapsed={() => toggleIsCollapsed('first', stateManagerMock)}
|
||||
collapseButtonRef={React.createRef()}
|
||||
{...propsOverrides}
|
||||
/>
|
||||
|
@ -62,36 +62,36 @@ describe('GridRowHeader', () => {
|
|||
|
||||
it('renders the panel count', async () => {
|
||||
const { component, gridLayoutStateManager } = renderGridRowHeader();
|
||||
const initialCount = component.getByTestId('kbnGridRowHeader-0--panelCount');
|
||||
const initialCount = component.getByTestId('kbnGridRowHeader-first--panelCount');
|
||||
expect(initialCount.textContent).toBe('(8 panels)');
|
||||
|
||||
act(() => {
|
||||
const currentRow = gridLayoutStateManager.gridLayout$.getValue()[0];
|
||||
gridLayoutStateManager.gridLayout$.next([
|
||||
{
|
||||
const currentRow = gridLayoutStateManager.gridLayout$.getValue().first;
|
||||
gridLayoutStateManager.gridLayout$.next({
|
||||
first: {
|
||||
...currentRow,
|
||||
panels: {
|
||||
panel1: currentRow.panels.panel1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const updatedCount = component.getByTestId('kbnGridRowHeader-0--panelCount');
|
||||
const updatedCount = component.getByTestId('kbnGridRowHeader-first--panelCount');
|
||||
expect(updatedCount.textContent).toBe('(1 panel)');
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking title calls `toggleIsCollapsed`', async () => {
|
||||
const { component, gridLayoutStateManager } = renderGridRowHeader();
|
||||
const title = component.getByTestId('kbnGridRowTitle-0');
|
||||
const title = component.getByTestId('kbnGridRowTitle-first');
|
||||
|
||||
expect(toggleIsCollapsed).toBeCalledTimes(0);
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue()[0].isCollapsed).toBe(false);
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue().first.isCollapsed).toBe(false);
|
||||
await userEvent.click(title);
|
||||
expect(toggleIsCollapsed).toBeCalledTimes(1);
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue()[0].isCollapsed).toBe(true);
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue().first.isCollapsed).toBe(true);
|
||||
});
|
||||
|
||||
describe('title editor', () => {
|
||||
|
@ -105,44 +105,44 @@ describe('GridRowHeader', () => {
|
|||
|
||||
it('clicking on edit icon triggers inline title editor and does not toggle collapsed', async () => {
|
||||
const { component, gridLayoutStateManager } = renderGridRowHeader();
|
||||
const editIcon = component.getByTestId('kbnGridRowTitle-0--edit');
|
||||
const editIcon = component.getByTestId('kbnGridRowTitle-first--edit');
|
||||
|
||||
expect(component.queryByTestId('kbnGridRowTitle-0--editor')).not.toBeInTheDocument();
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue()[0].isCollapsed).toBe(false);
|
||||
expect(component.queryByTestId('kbnGridRowTitle-first--editor')).not.toBeInTheDocument();
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue().first.isCollapsed).toBe(false);
|
||||
await userEvent.click(editIcon);
|
||||
expect(component.getByTestId('kbnGridRowTitle-0--editor')).toBeInTheDocument();
|
||||
expect(component.getByTestId('kbnGridRowTitle-first--editor')).toBeInTheDocument();
|
||||
expect(toggleIsCollapsed).toBeCalledTimes(0);
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue()[0].isCollapsed).toBe(false);
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue().first.isCollapsed).toBe(false);
|
||||
});
|
||||
|
||||
it('can update the title', async () => {
|
||||
const { component, gridLayoutStateManager } = renderGridRowHeader();
|
||||
expect(component.getByTestId('kbnGridRowTitle-0').textContent).toBe('Large section');
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section');
|
||||
expect(component.getByTestId('kbnGridRowTitle-first').textContent).toBe('Large section');
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue().first.title).toBe('Large section');
|
||||
|
||||
const editIcon = component.getByTestId('kbnGridRowTitle-0--edit');
|
||||
const editIcon = component.getByTestId('kbnGridRowTitle-first--edit');
|
||||
await userEvent.click(editIcon);
|
||||
await setTitle(component);
|
||||
const saveButton = component.getByTestId('euiInlineEditModeSaveButton');
|
||||
await userEvent.click(saveButton);
|
||||
|
||||
expect(component.queryByTestId('kbnGridRowTitle-0--editor')).not.toBeInTheDocument();
|
||||
expect(component.getByTestId('kbnGridRowTitle-0').textContent).toBe('Large section 123');
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section 123');
|
||||
expect(component.queryByTestId('kbnGridRowTitle-first--editor')).not.toBeInTheDocument();
|
||||
expect(component.getByTestId('kbnGridRowTitle-first').textContent).toBe('Large section 123');
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue().first.title).toBe('Large section 123');
|
||||
});
|
||||
|
||||
it('clicking on cancel closes the inline title editor without updating title', async () => {
|
||||
const { component, gridLayoutStateManager } = renderGridRowHeader();
|
||||
const editIcon = component.getByTestId('kbnGridRowTitle-0--edit');
|
||||
const editIcon = component.getByTestId('kbnGridRowTitle-first--edit');
|
||||
await userEvent.click(editIcon);
|
||||
|
||||
await setTitle(component);
|
||||
const cancelButton = component.getByTestId('euiInlineEditModeCancelButton');
|
||||
await userEvent.click(cancelButton);
|
||||
|
||||
expect(component.queryByTestId('kbnGridRowTitle-0--editor')).not.toBeInTheDocument();
|
||||
expect(component.getByTestId('kbnGridRowTitle-0').textContent).toBe('Large section');
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section');
|
||||
expect(component.queryByTestId('kbnGridRowTitle-first--editor')).not.toBeInTheDocument();
|
||||
expect(component.getByTestId('kbnGridRowTitle-first').textContent).toBe('Large section');
|
||||
expect(gridLayoutStateManager.gridLayout$.getValue().first.title).toBe('Large section');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,13 +26,13 @@ import { DeleteGridRowModal } from './delete_grid_row_modal';
|
|||
import { GridRowTitle } from './grid_row_title';
|
||||
|
||||
export interface GridRowHeaderProps {
|
||||
rowIndex: number;
|
||||
rowId: string;
|
||||
toggleIsCollapsed: () => void;
|
||||
collapseButtonRef: React.MutableRefObject<HTMLButtonElement | null>;
|
||||
}
|
||||
|
||||
export const GridRowHeader = React.memo(
|
||||
({ rowIndex, toggleIsCollapsed, collapseButtonRef }: GridRowHeaderProps) => {
|
||||
({ rowId, toggleIsCollapsed, collapseButtonRef }: GridRowHeaderProps) => {
|
||||
const { gridLayoutStateManager } = useGridLayoutContext();
|
||||
|
||||
const [editTitleOpen, setEditTitleOpen] = useState<boolean>(false);
|
||||
|
@ -41,7 +41,7 @@ export const GridRowHeader = React.memo(
|
|||
gridLayoutStateManager.accessMode$.getValue() === 'VIEW'
|
||||
);
|
||||
const [panelCount, setPanelCount] = useState<number>(
|
||||
Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length
|
||||
Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowId].panels).length
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -60,7 +60,7 @@ export const GridRowHeader = React.memo(
|
|||
*/
|
||||
const panelCountSubscription = gridLayoutStateManager.gridLayout$
|
||||
.pipe(
|
||||
map((layout) => Object.keys(layout[rowIndex]?.panels ?? {}).length),
|
||||
map((layout) => Object.keys(layout[rowId]?.panels ?? {}).length),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe((count) => {
|
||||
|
@ -71,23 +71,21 @@ export const GridRowHeader = React.memo(
|
|||
accessModeSubscription.unsubscribe();
|
||||
panelCountSubscription.unsubscribe();
|
||||
};
|
||||
}, [gridLayoutStateManager, rowIndex]);
|
||||
}, [gridLayoutStateManager, rowId]);
|
||||
|
||||
const confirmDeleteRow = useCallback(() => {
|
||||
/**
|
||||
* Memoization of this callback does not need to be dependant on the React panel count
|
||||
* state, so just grab the panel count via gridLayoutStateManager instead
|
||||
*/
|
||||
const count = Object.keys(
|
||||
gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels
|
||||
).length;
|
||||
const count = Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowId].panels).length;
|
||||
if (!Boolean(count)) {
|
||||
const newLayout = deleteRow(gridLayoutStateManager.gridLayout$.getValue(), rowIndex);
|
||||
const newLayout = deleteRow(gridLayoutStateManager.gridLayout$.getValue(), rowId);
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
} else {
|
||||
setDeleteModalVisible(true);
|
||||
}
|
||||
}, [gridLayoutStateManager.gridLayout$, rowIndex]);
|
||||
}, [gridLayoutStateManager.gridLayout$, rowId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -97,10 +95,10 @@ export const GridRowHeader = React.memo(
|
|||
alignItems="center"
|
||||
css={styles.headerStyles}
|
||||
className="kbnGridRowHeader"
|
||||
data-test-subj={`kbnGridRowHeader-${rowIndex}`}
|
||||
data-test-subj={`kbnGridRowHeader-${rowId}`}
|
||||
>
|
||||
<GridRowTitle
|
||||
rowIndex={rowIndex}
|
||||
rowId={rowId}
|
||||
readOnly={readOnly}
|
||||
toggleIsCollapsed={toggleIsCollapsed}
|
||||
editTitleOpen={editTitleOpen}
|
||||
|
@ -118,7 +116,7 @@ export const GridRowHeader = React.memo(
|
|||
<EuiText
|
||||
color="subdued"
|
||||
size="s"
|
||||
data-test-subj={`kbnGridRowHeader-${rowIndex}--panelCount`}
|
||||
data-test-subj={`kbnGridRowHeader-${rowId}--panelCount`}
|
||||
className={'kbnGridLayout--panelCount'}
|
||||
>
|
||||
{i18n.translate('kbnGridLayout.rowHeader.panelCount', {
|
||||
|
@ -166,7 +164,7 @@ export const GridRowHeader = React.memo(
|
|||
}
|
||||
</EuiFlexGroup>
|
||||
{deleteModalVisible && (
|
||||
<DeleteGridRowModal rowIndex={rowIndex} setDeleteModalVisible={setDeleteModalVisible} />
|
||||
<DeleteGridRowModal rowId={rowId} setDeleteModalVisible={setDeleteModalVisible} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -26,14 +26,14 @@ import { useGridLayoutContext } from '../use_grid_layout_context';
|
|||
export const GridRowTitle = React.memo(
|
||||
({
|
||||
readOnly,
|
||||
rowIndex,
|
||||
rowId,
|
||||
editTitleOpen,
|
||||
setEditTitleOpen,
|
||||
toggleIsCollapsed,
|
||||
collapseButtonRef,
|
||||
}: {
|
||||
readOnly: boolean;
|
||||
rowIndex: number;
|
||||
rowId: string;
|
||||
editTitleOpen: boolean;
|
||||
setEditTitleOpen: (value: boolean) => void;
|
||||
toggleIsCollapsed: () => void;
|
||||
|
@ -42,7 +42,7 @@ export const GridRowTitle = React.memo(
|
|||
const { gridLayoutStateManager } = useGridLayoutContext();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex];
|
||||
const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowId];
|
||||
const [rowTitle, setRowTitle] = useState<string>(currentRow.title);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -51,7 +51,7 @@ export const GridRowTitle = React.memo(
|
|||
*/
|
||||
const titleSubscription = gridLayoutStateManager.gridLayout$
|
||||
.pipe(
|
||||
map((gridLayout) => gridLayout[rowIndex]?.title ?? ''),
|
||||
map((gridLayout) => gridLayout[rowId]?.title ?? ''),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe((title) => {
|
||||
|
@ -61,7 +61,7 @@ export const GridRowTitle = React.memo(
|
|||
return () => {
|
||||
titleSubscription.unsubscribe();
|
||||
};
|
||||
}, [rowIndex, gridLayoutStateManager]);
|
||||
}, [rowId, gridLayoutStateManager]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
|
@ -75,11 +75,11 @@ export const GridRowTitle = React.memo(
|
|||
const updateTitle = useCallback(
|
||||
(title: string) => {
|
||||
const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.getValue());
|
||||
newLayout[rowIndex].title = title;
|
||||
newLayout[rowId].title = title;
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
setEditTitleOpen(false);
|
||||
},
|
||||
[rowIndex, setEditTitleOpen, gridLayoutStateManager.gridLayout$]
|
||||
[rowId, setEditTitleOpen, gridLayoutStateManager.gridLayout$]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -94,9 +94,9 @@ export const GridRowTitle = React.memo(
|
|||
iconType={'arrowDown'}
|
||||
onClick={toggleIsCollapsed}
|
||||
size="m"
|
||||
id={`kbnGridRowTitle-${rowIndex}`}
|
||||
aria-controls={`kbnGridRow-${rowIndex}`}
|
||||
data-test-subj={`kbnGridRowTitle-${rowIndex}`}
|
||||
id={`kbnGridRowTitle-${rowId}`}
|
||||
aria-controls={`kbnGridRow-${rowId}`}
|
||||
data-test-subj={`kbnGridRowTitle-${rowId}`}
|
||||
textProps={false}
|
||||
flush="both"
|
||||
>
|
||||
|
@ -123,7 +123,7 @@ export const GridRowTitle = React.memo(
|
|||
inputAriaLabel={i18n.translate('kbnGridLayout.row.editTitleAriaLabel', {
|
||||
defaultMessage: 'Edit section title',
|
||||
})}
|
||||
data-test-subj={`kbnGridRowTitle-${rowIndex}--editor`}
|
||||
data-test-subj={`kbnGridRowTitle-${rowId}--editor`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
|
@ -137,7 +137,7 @@ export const GridRowTitle = React.memo(
|
|||
aria-label={i18n.translate('kbnGridLayout.row.editRowTitle', {
|
||||
defaultMessage: 'Edit section title',
|
||||
})}
|
||||
data-test-subj={`kbnGridRowTitle-${rowIndex}--edit`}
|
||||
data-test-subj={`kbnGridRowTitle-${rowId}--edit`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
|
|
@ -9,10 +9,12 @@
|
|||
|
||||
import { GridLayoutData } from '../types';
|
||||
|
||||
export const getSampleLayout = (): GridLayoutData => [
|
||||
{
|
||||
export const getSampleLayout = (): GridLayoutData => ({
|
||||
first: {
|
||||
title: 'Large section',
|
||||
isCollapsed: false,
|
||||
id: 'first',
|
||||
order: 0,
|
||||
panels: {
|
||||
panel1: {
|
||||
id: 'panel1',
|
||||
|
@ -72,9 +74,11 @@ export const getSampleLayout = (): GridLayoutData => [
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
second: {
|
||||
title: 'Small section',
|
||||
isCollapsed: false,
|
||||
id: 'second',
|
||||
order: 1,
|
||||
panels: {
|
||||
panel9: {
|
||||
id: 'panel9',
|
||||
|
@ -85,9 +89,11 @@ export const getSampleLayout = (): GridLayoutData => [
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
third: {
|
||||
title: 'Another small section',
|
||||
isCollapsed: false,
|
||||
id: 'third',
|
||||
order: 2,
|
||||
panels: {
|
||||
panel10: {
|
||||
id: 'panel10',
|
||||
|
@ -98,4 +104,4 @@ export const getSampleLayout = (): GridLayoutData => [
|
|||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
|
|
@ -24,6 +24,8 @@ export interface GridPanelData extends GridRect {
|
|||
}
|
||||
|
||||
export interface GridRowData {
|
||||
id: string;
|
||||
order: number;
|
||||
title: string;
|
||||
isCollapsed: boolean;
|
||||
panels: {
|
||||
|
@ -31,7 +33,9 @@ export interface GridRowData {
|
|||
};
|
||||
}
|
||||
|
||||
export type GridLayoutData = GridRowData[];
|
||||
export interface GridLayoutData {
|
||||
[rowId: string]: GridRowData;
|
||||
}
|
||||
|
||||
export interface GridSettings {
|
||||
gutterSize: number;
|
||||
|
@ -67,8 +71,10 @@ export interface GridLayoutStateManager {
|
|||
activePanel$: BehaviorSubject<ActivePanel | undefined>;
|
||||
interactionEvent$: BehaviorSubject<PanelInteractionEvent | undefined>;
|
||||
|
||||
rowRefs: React.MutableRefObject<Array<HTMLDivElement | null>>;
|
||||
panelRefs: React.MutableRefObject<Array<{ [id: string]: HTMLDivElement | null }>>;
|
||||
rowRefs: React.MutableRefObject<{ [rowId: string]: HTMLDivElement | null }>;
|
||||
panelRefs: React.MutableRefObject<{
|
||||
[rowId: string]: { [panelId: string]: HTMLDivElement | null };
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,7 +94,7 @@ export interface PanelInteractionEvent {
|
|||
/**
|
||||
* The index of the grid row this panel interaction is targeting.
|
||||
*/
|
||||
targetRowIndex: number;
|
||||
targetRow: string;
|
||||
|
||||
/**
|
||||
* The pixel rect of the panel being interacted with.
|
||||
|
|
|
@ -29,11 +29,11 @@ import { useGridLayoutContext } from '../use_grid_layout_context';
|
|||
|
||||
export const useGridLayoutEvents = ({
|
||||
interactionType,
|
||||
rowIndex,
|
||||
rowId,
|
||||
panelId,
|
||||
}: {
|
||||
interactionType: PanelInteractionEvent['type'];
|
||||
rowIndex: number;
|
||||
rowId: string;
|
||||
panelId: string;
|
||||
}) => {
|
||||
const { gridLayoutStateManager } = useGridLayoutContext();
|
||||
|
@ -45,8 +45,7 @@ export const useGridLayoutEvents = ({
|
|||
(e: UserInteractionEvent) => {
|
||||
if (!isLayoutInteractive(gridLayoutStateManager)) return;
|
||||
|
||||
const onStart = () =>
|
||||
startAction(e, gridLayoutStateManager, interactionType, rowIndex, panelId);
|
||||
const onStart = () => startAction(e, gridLayoutStateManager, interactionType, rowId, panelId);
|
||||
|
||||
const onMove = (ev: UserInteractionEvent) => {
|
||||
if (isMouseEvent(ev) || isTouchEvent(ev)) {
|
||||
|
@ -74,7 +73,7 @@ export const useGridLayoutEvents = ({
|
|||
});
|
||||
}
|
||||
},
|
||||
[gridLayoutStateManager, rowIndex, panelId, interactionType]
|
||||
[gridLayoutStateManager, rowId, panelId, interactionType]
|
||||
);
|
||||
|
||||
return startInteraction;
|
||||
|
|
|
@ -20,10 +20,10 @@ export const startAction = (
|
|||
e: UserInteractionEvent,
|
||||
gridLayoutStateManager: GridLayoutStateManager,
|
||||
type: 'drag' | 'resize',
|
||||
rowIndex: number,
|
||||
rowId: string,
|
||||
panelId: string
|
||||
) => {
|
||||
const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
const panelRef = gridLayoutStateManager.panelRefs.current[rowId][panelId];
|
||||
if (!panelRef) return;
|
||||
|
||||
const panelRect = panelRef.getBoundingClientRect();
|
||||
|
@ -32,7 +32,7 @@ export const startAction = (
|
|||
type,
|
||||
id: panelId,
|
||||
panelDiv: panelRef,
|
||||
targetRowIndex: rowIndex,
|
||||
targetRow: rowId,
|
||||
pointerOffsets: getPointerOffsets(e, panelRect),
|
||||
});
|
||||
|
||||
|
@ -74,8 +74,7 @@ export const moveAction = (
|
|||
|
||||
const currentLayout = proposedGridLayout$.value;
|
||||
|
||||
const currentPanelData =
|
||||
currentLayout?.[interactionEvent.targetRowIndex].panels[interactionEvent.id];
|
||||
const currentPanelData = currentLayout?.[interactionEvent.targetRow].panels[interactionEvent.id];
|
||||
|
||||
if (!currentPanelData) {
|
||||
return;
|
||||
|
@ -100,37 +99,37 @@ export const moveAction = (
|
|||
const { columnCount, gutterSize, rowHeight, columnPixelWidth } = runtimeSettings;
|
||||
|
||||
// find the grid that the preview rect is over
|
||||
const lastRowIndex = interactionEvent.targetRowIndex;
|
||||
const targetRowIndex = (() => {
|
||||
if (isResize) return lastRowIndex;
|
||||
const lastRowId = interactionEvent.targetRow;
|
||||
const targetRowId = (() => {
|
||||
if (isResize) return lastRowId;
|
||||
const previewBottom = previewRect.top + rowHeight;
|
||||
|
||||
let highestOverlap = -Infinity;
|
||||
let highestOverlapRowIndex = -1;
|
||||
gridRowElements.forEach((row, index) => {
|
||||
let highestOverlapRowId = '';
|
||||
Object.entries(gridRowElements).forEach(([id, row]) => {
|
||||
if (!row) return;
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const overlap =
|
||||
Math.min(previewBottom, rowRect.bottom) - Math.max(previewRect.top, rowRect.top);
|
||||
if (overlap > highestOverlap) {
|
||||
highestOverlap = overlap;
|
||||
highestOverlapRowIndex = index;
|
||||
highestOverlapRowId = id;
|
||||
}
|
||||
});
|
||||
return highestOverlapRowIndex;
|
||||
return highestOverlapRowId;
|
||||
})();
|
||||
const hasChangedGridRow = targetRowIndex !== lastRowIndex;
|
||||
const hasChangedGridRow = targetRowId !== lastRowId;
|
||||
|
||||
// re-render when the target row changes
|
||||
if (hasChangedGridRow) {
|
||||
interactionEvent$.next({
|
||||
...interactionEvent,
|
||||
targetRowIndex,
|
||||
targetRow: targetRowId,
|
||||
});
|
||||
}
|
||||
|
||||
// calculate the requested grid position
|
||||
const targetedGridRow = gridRowElements[targetRowIndex];
|
||||
const targetedGridRow = gridRowElements[targetRowId];
|
||||
const targetedGridLeft = targetedGridRow?.getBoundingClientRect().left ?? 0;
|
||||
const targetedGridTop = targetedGridRow?.getBoundingClientRect().top ?? 0;
|
||||
|
||||
|
@ -166,21 +165,22 @@ export const moveAction = (
|
|||
lastRequestedPanelPosition.current = { ...requestedPanelData };
|
||||
|
||||
// remove the panel from the row it's currently in.
|
||||
const nextLayout = currentLayout.map((row) => {
|
||||
const nextLayout = cloneDeep(currentLayout);
|
||||
Object.entries(nextLayout).forEach(([rowId, row]) => {
|
||||
const { [interactionEvent.id]: interactingPanel, ...otherPanels } = row.panels;
|
||||
return { ...row, panels: { ...otherPanels } };
|
||||
nextLayout[rowId] = { ...row, panels: { ...otherPanels } };
|
||||
});
|
||||
|
||||
// resolve destination grid
|
||||
const destinationGrid = nextLayout[targetRowIndex];
|
||||
const destinationGrid = nextLayout[targetRowId];
|
||||
const resolvedDestinationGrid = resolveGridRow(destinationGrid, requestedPanelData);
|
||||
nextLayout[targetRowIndex] = resolvedDestinationGrid;
|
||||
nextLayout[targetRowId] = resolvedDestinationGrid;
|
||||
|
||||
// resolve origin grid
|
||||
if (hasChangedGridRow) {
|
||||
const originGrid = nextLayout[lastRowIndex];
|
||||
const originGrid = nextLayout[lastRowId];
|
||||
const resolvedOriginGrid = resolveGridRow(originGrid);
|
||||
nextLayout[lastRowIndex] = resolvedOriginGrid;
|
||||
nextLayout[lastRowId] = resolvedOriginGrid;
|
||||
}
|
||||
if (!deepEqual(currentLayout, nextLayout)) {
|
||||
proposedGridLayout$.next(nextLayout);
|
||||
|
|
|
@ -42,8 +42,8 @@ export const useGridLayoutState = ({
|
|||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
setDimensionsRef: (instance: HTMLDivElement | null) => void;
|
||||
} => {
|
||||
const rowRefs = useRef<Array<HTMLDivElement | null>>([]);
|
||||
const panelRefs = useRef<Array<{ [id: string]: HTMLDivElement | null }>>([]);
|
||||
const rowRefs = useRef<{ [rowId: string]: HTMLDivElement | null }>({});
|
||||
const panelRefs = useRef<{ [rowId: string]: { [panelId: string]: HTMLDivElement | null } }>({});
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const expandedPanelId$ = useMemo(
|
||||
|
@ -84,8 +84,8 @@ export const useGridLayoutState = ({
|
|||
|
||||
const gridLayoutStateManager = useMemo(() => {
|
||||
const resolvedLayout = cloneDeep(layout);
|
||||
resolvedLayout.forEach((row, rowIndex) => {
|
||||
resolvedLayout[rowIndex] = resolveGridRow(row);
|
||||
Object.values(resolvedLayout).forEach((row) => {
|
||||
resolvedLayout[row.id] = resolveGridRow(row);
|
||||
});
|
||||
|
||||
const gridLayout$ = new BehaviorSubject<GridLayoutData>(resolvedLayout);
|
||||
|
@ -93,14 +93,10 @@ export const useGridLayoutState = ({
|
|||
const gridDimensions$ = new BehaviorSubject<ObservedSize>({ width: 0, height: 0 });
|
||||
const interactionEvent$ = new BehaviorSubject<PanelInteractionEvent | undefined>(undefined);
|
||||
const activePanel$ = new BehaviorSubject<ActivePanel | undefined>(undefined);
|
||||
const panelIds$ = new BehaviorSubject<string[][]>(
|
||||
layout.map(({ panels }) => Object.keys(panels))
|
||||
);
|
||||
|
||||
return {
|
||||
rowRefs,
|
||||
panelRefs,
|
||||
panelIds$,
|
||||
proposedGridLayout$,
|
||||
gridLayout$,
|
||||
activePanel$,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { GridLayoutData, GridPanelData } from '../types';
|
||||
|
||||
export const isGridDataEqual = (a?: GridPanelData, b?: GridPanelData) => {
|
||||
|
@ -20,14 +21,16 @@ export const isGridDataEqual = (a?: GridPanelData, b?: GridPanelData) => {
|
|||
};
|
||||
|
||||
export const isLayoutEqual = (a: GridLayoutData, b: GridLayoutData) => {
|
||||
if (a.length !== b.length) return false;
|
||||
if (!deepEqual(Object.keys(a), Object.keys(b))) return false;
|
||||
|
||||
let isEqual = true;
|
||||
for (let rowIndex = 0; rowIndex < a.length && isEqual; rowIndex++) {
|
||||
const rowA = a[rowIndex];
|
||||
const rowB = b[rowIndex];
|
||||
const keys = Object.keys(a); // keys of A are equal to keys of b
|
||||
for (const key of keys) {
|
||||
const rowA = a[key];
|
||||
const rowB = b[key];
|
||||
|
||||
isEqual =
|
||||
rowA.order === rowB.order &&
|
||||
rowA.title === rowB.title &&
|
||||
rowA.isCollapsed === rowB.isCollapsed &&
|
||||
Object.keys(rowA.panels).length === Object.keys(rowB.panels).length;
|
||||
|
@ -38,6 +41,7 @@ export const isLayoutEqual = (a: GridLayoutData, b: GridLayoutData) => {
|
|||
if (!isEqual) break;
|
||||
}
|
||||
}
|
||||
if (!isEqual) break;
|
||||
}
|
||||
|
||||
return isEqual;
|
||||
|
|
|
@ -12,6 +12,8 @@ import { resolveGridRow } from './resolve_grid_row';
|
|||
describe('resolve grid row', () => {
|
||||
test('does nothing if grid row has no collisions', () => {
|
||||
const gridRow = {
|
||||
order: 0,
|
||||
id: 'first',
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
|
@ -27,6 +29,8 @@ describe('resolve grid row', () => {
|
|||
|
||||
test('resolves grid row if it has collisions without drag event', () => {
|
||||
const result = resolveGridRow({
|
||||
order: 0,
|
||||
id: 'first',
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
|
@ -37,6 +41,8 @@ describe('resolve grid row', () => {
|
|||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
order: 0,
|
||||
id: 'first',
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
|
@ -51,6 +57,8 @@ describe('resolve grid row', () => {
|
|||
test('drag causes no collision', () => {
|
||||
const result = resolveGridRow(
|
||||
{
|
||||
order: 0,
|
||||
id: 'first',
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
|
@ -63,6 +71,8 @@ describe('resolve grid row', () => {
|
|||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
order: 0,
|
||||
id: 'first',
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
|
@ -77,6 +87,8 @@ describe('resolve grid row', () => {
|
|||
test('drag causes collision with one panel that pushes down others', () => {
|
||||
const result = resolveGridRow(
|
||||
{
|
||||
order: 0,
|
||||
id: 'first',
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
|
@ -90,6 +102,8 @@ describe('resolve grid row', () => {
|
|||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
order: 0,
|
||||
id: 'first',
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
|
@ -105,6 +119,8 @@ describe('resolve grid row', () => {
|
|||
test('drag causes collision with multiple panels', () => {
|
||||
const result = resolveGridRow(
|
||||
{
|
||||
order: 0,
|
||||
id: 'first',
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
|
@ -116,6 +132,8 @@ describe('resolve grid row', () => {
|
|||
{ id: 'panel4', row: 0, column: 3, height: 5, width: 4 }
|
||||
);
|
||||
expect(result).toEqual({
|
||||
order: 0,
|
||||
id: 'first',
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
|
@ -130,6 +148,8 @@ describe('resolve grid row', () => {
|
|||
test('drag causes collision with every panel', () => {
|
||||
const result = resolveGridRow(
|
||||
{
|
||||
order: 0,
|
||||
id: 'first',
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
|
@ -142,6 +162,8 @@ describe('resolve grid row', () => {
|
|||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
order: 0,
|
||||
id: 'first',
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { GridPanelData, GridRowData } from '../types';
|
||||
import { GridLayoutData, GridPanelData, GridRowData } from '../types';
|
||||
|
||||
const collides = (panelA: GridPanelData, panelB: GridPanelData) => {
|
||||
if (panelA.id === panelB.id) return false; // same panel
|
||||
|
@ -46,7 +46,16 @@ const getFirstCollision = (gridLayout: GridRowData, keysInOrder: string[]): stri
|
|||
return undefined;
|
||||
};
|
||||
|
||||
export const getKeysInOrder = (panels: GridRowData['panels'], draggedId?: string): string[] => {
|
||||
export const getRowKeysInOrder = (rows: GridLayoutData): string[] => {
|
||||
return Object.values(rows)
|
||||
.sort(({ order: orderA }, { order: orderB }) => orderA - orderB)
|
||||
.map(({ id }) => id);
|
||||
};
|
||||
|
||||
export const getPanelKeysInOrder = (
|
||||
panels: GridRowData['panels'],
|
||||
draggedId?: string
|
||||
): string[] => {
|
||||
const panelKeys = Object.keys(panels);
|
||||
return panelKeys.sort((panelKeyA, panelKeyB) => {
|
||||
const panelA = panels[panelKeyA];
|
||||
|
@ -72,7 +81,7 @@ export const getKeysInOrder = (panels: GridRowData['panels'], draggedId?: string
|
|||
const compactGridRow = (originalLayout: GridRowData) => {
|
||||
const nextRowData = { ...originalLayout, panels: { ...originalLayout.panels } };
|
||||
// compact all vertical space.
|
||||
const sortedKeysAfterMove = getKeysInOrder(nextRowData.panels);
|
||||
const sortedKeysAfterMove = getPanelKeysInOrder(nextRowData.panels);
|
||||
for (const panelKey of sortedKeysAfterMove) {
|
||||
const panel = nextRowData.panels[panelKey];
|
||||
// try moving panel up one row at a time until it collides
|
||||
|
@ -99,7 +108,7 @@ export const resolveGridRow = (
|
|||
nextRowData.panels[dragRequest.id] = dragRequest;
|
||||
}
|
||||
// get keys in order from top to bottom, left to right, with priority on the dragged item if it exists
|
||||
const sortedKeys = getKeysInOrder(nextRowData.panels, dragRequest?.id);
|
||||
const sortedKeys = getPanelKeysInOrder(nextRowData.panels, dragRequest?.id);
|
||||
|
||||
// while the layout has at least one collision, try to resolve them in order
|
||||
let collision = getFirstCollision(nextRowData, sortedKeys);
|
||||
|
|
|
@ -18,7 +18,7 @@ import { resolveGridRow } from './resolve_grid_row';
|
|||
* @param newRow The destination row for the panels
|
||||
* @returns Updated layout with panels moved from `startingRow` to `newRow`
|
||||
*/
|
||||
export const movePanelsToRow = (layout: GridLayoutData, startingRow: number, newRow: number) => {
|
||||
export const movePanelsToRow = (layout: GridLayoutData, startingRow: string, newRow: string) => {
|
||||
const newLayout = cloneDeep(layout);
|
||||
const panelsToMove = newLayout[startingRow].panels;
|
||||
const maxRow = Math.max(
|
||||
|
@ -37,8 +37,8 @@ export const movePanelsToRow = (layout: GridLayoutData, startingRow: number, new
|
|||
* @param rowIndex The row to be deleted
|
||||
* @returns Updated layout with the row at `rowIndex` deleted
|
||||
*/
|
||||
export const deleteRow = (layout: GridLayoutData, rowIndex: number) => {
|
||||
export const deleteRow = (layout: GridLayoutData, rowId: string) => {
|
||||
const newLayout = cloneDeep(layout);
|
||||
newLayout.splice(rowIndex, 1);
|
||||
delete newLayout[rowId];
|
||||
return newLayout;
|
||||
};
|
||||
|
|
|
@ -9,13 +9,14 @@
|
|||
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { useAppFixedViewport } from '@kbn/core-rendering-browser';
|
||||
import { GridLayout, type GridLayoutData } from '@kbn/grid-layout';
|
||||
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
|
||||
import { DashboardPanelState } from '../../../../common';
|
||||
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../../common/content_management/constants';
|
||||
import { arePanelLayoutsEqual } from '../../../dashboard_api/are_panel_layouts_equal';
|
||||
|
@ -33,6 +34,7 @@ export const DashboardGrid = ({
|
|||
const layoutStyles = useLayoutStyles();
|
||||
const panelRefs = useRef<{ [panelId: string]: React.Ref<HTMLDivElement> }>({});
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const firstRowId = useRef(uuidv4());
|
||||
|
||||
const [expandedPanelId, panels, useMargins, viewMode] = useBatchedPublishingSubjects(
|
||||
dashboardApi.expandedPanelId$,
|
||||
|
@ -44,7 +46,9 @@ export const DashboardGrid = ({
|
|||
const appFixedViewport = useAppFixedViewport();
|
||||
|
||||
const currentLayout: GridLayoutData = useMemo(() => {
|
||||
const singleRow: GridLayoutData[number] = {
|
||||
const singleRow: GridLayoutData[string] = {
|
||||
id: firstRowId.current,
|
||||
order: 0,
|
||||
title: '', // we only support a single section currently, and it does not have a title
|
||||
isCollapsed: false,
|
||||
panels: {},
|
||||
|
@ -66,7 +70,7 @@ export const DashboardGrid = ({
|
|||
}
|
||||
});
|
||||
|
||||
return [singleRow];
|
||||
return { [firstRowId.current]: singleRow };
|
||||
}, [panels]);
|
||||
|
||||
const onLayoutChange = useCallback(
|
||||
|
@ -75,7 +79,7 @@ export const DashboardGrid = ({
|
|||
|
||||
const currentPanels = dashboardApi.panels$.getValue();
|
||||
const updatedPanels: { [key: string]: DashboardPanelState } = Object.values(
|
||||
newLayout[0].panels
|
||||
newLayout[firstRowId.current].panels
|
||||
).reduce((updatedPanelsAcc, panelLayout) => {
|
||||
updatedPanelsAcc[panelLayout.id] = {
|
||||
...currentPanels[panelLayout.id],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue