mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Dashboard][Collapsable Panels] New collision resolution algorithm](https://github.com/elastic/kibana/pull/204134) <!--- Backport version: 8.9.8 -->
This commit is contained in:
parent
eca9ebccac
commit
1ab448126b
5 changed files with 222 additions and 27 deletions
|
@ -69,15 +69,19 @@ export const GridExample = ({
|
|||
combineLatest([mockDashboardApi.panels$, mockDashboardApi.rows$])
|
||||
.pipe(debounceTime(0)) // debounce to avoid subscribe being called twice when both panels$ and rows$ publish
|
||||
.subscribe(([panels, rows]) => {
|
||||
const hasChanges = !(
|
||||
deepEqual(
|
||||
Object.values(panels).map(({ gridData }) => ({ row: 0, ...gridData })),
|
||||
Object.values(savedState.current.panels).map(({ gridData }) => ({
|
||||
row: 0, // if row is undefined, then default to 0
|
||||
...gridData,
|
||||
}))
|
||||
) && deepEqual(rows, savedState.current.rows)
|
||||
);
|
||||
const panelIds = Object.keys(panels);
|
||||
let panelsAreEqual = true;
|
||||
for (const panelId of panelIds) {
|
||||
if (!panelsAreEqual) break;
|
||||
const currentPanel = panels[panelId];
|
||||
const savedPanel = savedState.current.panels[panelId];
|
||||
panelsAreEqual = deepEqual(
|
||||
{ row: 0, ...currentPanel.gridData },
|
||||
{ row: 0, ...savedPanel.gridData }
|
||||
);
|
||||
}
|
||||
|
||||
const hasChanges = !(panelsAreEqual && deepEqual(rows, savedState.current.rows));
|
||||
setHasUnsavedChanges(hasChanges);
|
||||
setCurrentLayout(dashboardInputToGridLayout({ panels, rows }));
|
||||
});
|
||||
|
|
|
@ -46,6 +46,8 @@ export const useMockDashboardApi = ({
|
|||
from: 'now-24h',
|
||||
to: 'now',
|
||||
}),
|
||||
filters$: new BehaviorSubject([]),
|
||||
query$: new BehaviorSubject(''),
|
||||
viewMode: new BehaviorSubject('edit'),
|
||||
panels$,
|
||||
rows$: new BehaviorSubject<MockedDashboardRowMap>(savedState.rows),
|
||||
|
|
|
@ -32,6 +32,7 @@ describe('GridLayout', () => {
|
|||
rerender(<GridLayout {...defaultProps} {...overrides} />),
|
||||
};
|
||||
};
|
||||
|
||||
const getAllThePanelIds = () =>
|
||||
screen
|
||||
.getAllByRole('button', { name: /panelId:panel/i })
|
||||
|
@ -40,9 +41,11 @@ describe('GridLayout', () => {
|
|||
const startDragging = (handle: HTMLElement, options = { clientX: 0, clientY: 0 }) => {
|
||||
fireEvent.mouseDown(handle, options);
|
||||
};
|
||||
|
||||
const moveTo = (options = { clientX: 256, clientY: 128 }) => {
|
||||
fireEvent.mouseMove(document, options);
|
||||
};
|
||||
|
||||
const drop = (handle: HTMLElement) => {
|
||||
fireEvent.mouseUp(handle);
|
||||
};
|
||||
|
@ -123,6 +126,7 @@ describe('GridLayout', () => {
|
|||
'panel10',
|
||||
]);
|
||||
});
|
||||
|
||||
it('after removing a panel', async () => {
|
||||
const { rerender } = renderGridLayout();
|
||||
const sampleLayoutWithoutPanel1 = cloneDeep(getSampleLayout());
|
||||
|
@ -141,6 +145,7 @@ describe('GridLayout', () => {
|
|||
'panel10',
|
||||
]);
|
||||
});
|
||||
|
||||
it('after replacing a panel id', async () => {
|
||||
const { rerender } = renderGridLayout();
|
||||
const modifiedLayout = cloneDeep(getSampleLayout());
|
||||
|
|
155
packages/kbn-grid-layout/grid/utils/resolve_grid_row.test.ts
Normal file
155
packages/kbn-grid-layout/grid/utils/resolve_grid_row.test.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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 { resolveGridRow } from './resolve_grid_row';
|
||||
|
||||
describe('resolve grid row', () => {
|
||||
test('does nothing if grid row has no collisions', () => {
|
||||
const gridRow = {
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { id: 'panel1', row: 3, column: 0, height: 1, width: 7 },
|
||||
panel2: { id: 'panel2', row: 4, column: 0, height: 1, width: 7 },
|
||||
panel3: { id: 'panel3', row: 5, column: 0, height: 1, width: 7 },
|
||||
panel4: { id: 'panel4', row: 0, column: 6, height: 3, width: 1 },
|
||||
},
|
||||
};
|
||||
const result = resolveGridRow(gridRow);
|
||||
expect(result).toEqual(gridRow);
|
||||
});
|
||||
|
||||
test('resolves grid row if it has collisions without drag event', () => {
|
||||
const result = resolveGridRow({
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { id: 'panel1', row: 0, column: 0, height: 3, width: 4 },
|
||||
panel2: { id: 'panel2', row: 3, column: 0, height: 2, width: 2 },
|
||||
panel3: { id: 'panel3', row: 3, column: 2, height: 2, width: 2 },
|
||||
panel4: { id: 'panel4', row: 0, column: 3, height: 5, width: 4 },
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { id: 'panel1', row: 0, column: 0, height: 3, width: 4 },
|
||||
panel2: { id: 'panel2', row: 3, column: 0, height: 2, width: 2 },
|
||||
panel3: { id: 'panel3', row: 8, column: 2, height: 2, width: 2 }, // pushed down
|
||||
panel4: { id: 'panel4', row: 3, column: 3, height: 5, width: 4 }, // pushed down
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('drag causes no collision', () => {
|
||||
const result = resolveGridRow(
|
||||
{
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { id: 'panel1', row: 0, column: 0, height: 1, width: 7 },
|
||||
panel2: { id: 'panel2', row: 1, column: 0, height: 1, width: 7 },
|
||||
panel3: { id: 'panel3', row: 2, column: 0, height: 1, width: 7 },
|
||||
},
|
||||
},
|
||||
{ id: 'panel4', row: 0, column: 7, height: 3, width: 1 }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { id: 'panel1', row: 0, column: 0, height: 1, width: 7 },
|
||||
panel2: { id: 'panel2', row: 1, column: 0, height: 1, width: 7 },
|
||||
panel3: { id: 'panel3', row: 2, column: 0, height: 1, width: 7 },
|
||||
panel4: { id: 'panel4', row: 0, column: 7, height: 3, width: 1 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('drag causes collision with one panel that pushes down others', () => {
|
||||
const result = resolveGridRow(
|
||||
{
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { id: 'panel1', row: 0, column: 0, height: 1, width: 7 },
|
||||
panel2: { id: 'panel2', row: 1, column: 0, height: 1, width: 7 },
|
||||
panel3: { id: 'panel3', row: 2, column: 0, height: 1, width: 8 },
|
||||
panel4: { id: 'panel4', row: 3, column: 4, height: 3, width: 4 },
|
||||
},
|
||||
},
|
||||
{ id: 'panel5', row: 2, column: 0, height: 3, width: 3 }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { id: 'panel1', row: 0, column: 0, height: 1, width: 7 },
|
||||
panel2: { id: 'panel2', row: 1, column: 0, height: 1, width: 7 },
|
||||
panel3: { id: 'panel3', row: 5, column: 0, height: 1, width: 8 }, // pushed down
|
||||
panel4: { id: 'panel4', row: 6, column: 4, height: 3, width: 4 }, // pushed down
|
||||
panel5: { id: 'panel5', row: 2, column: 0, height: 3, width: 3 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('drag causes collision with multiple panels', () => {
|
||||
const result = resolveGridRow(
|
||||
{
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { id: 'panel1', row: 0, column: 0, height: 3, width: 4 },
|
||||
panel2: { id: 'panel2', row: 3, column: 0, height: 2, width: 2 },
|
||||
panel3: { id: 'panel3', row: 3, column: 2, height: 2, width: 2 },
|
||||
},
|
||||
},
|
||||
{ id: 'panel4', row: 0, column: 3, height: 5, width: 4 }
|
||||
);
|
||||
expect(result).toEqual({
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { id: 'panel1', row: 5, column: 0, height: 3, width: 4 }, // pushed down
|
||||
panel2: { id: 'panel2', row: 8, column: 0, height: 2, width: 2 }, // pushed down
|
||||
panel3: { id: 'panel3', row: 8, column: 2, height: 2, width: 2 }, // pushed down
|
||||
panel4: { id: 'panel4', row: 0, column: 3, height: 5, width: 4 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('drag causes collision with every panel', () => {
|
||||
const result = resolveGridRow(
|
||||
{
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { id: 'panel1', row: 0, column: 0, height: 1, width: 7 },
|
||||
panel2: { id: 'panel2', row: 1, column: 0, height: 1, width: 7 },
|
||||
panel3: { id: 'panel3', row: 2, column: 0, height: 1, width: 7 },
|
||||
},
|
||||
},
|
||||
{ id: 'panel4', row: 0, column: 6, height: 3, width: 1 }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
title: 'Test',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { id: 'panel1', row: 3, column: 0, height: 1, width: 7 },
|
||||
panel2: { id: 'panel2', row: 4, column: 0, height: 1, width: 7 },
|
||||
panel3: { id: 'panel3', row: 5, column: 0, height: 1, width: 7 },
|
||||
panel4: { id: 'panel4', row: 0, column: 6, height: 3, width: 1 },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -34,6 +34,18 @@ const getAllCollisionsWithPanel = (
|
|||
return collidingPanels;
|
||||
};
|
||||
|
||||
const getFirstCollision = (gridLayout: GridRowData, keysInOrder: string[]): string | undefined => {
|
||||
for (const panelA of keysInOrder) {
|
||||
for (const panelB of keysInOrder) {
|
||||
if (panelA === panelB) continue;
|
||||
if (collides(gridLayout.panels[panelA], gridLayout.panels[panelB])) {
|
||||
return panelA;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getKeysInOrder = (panels: GridRowData['panels'], draggedId?: string): string[] => {
|
||||
const panelKeys = Object.keys(panels);
|
||||
return panelKeys.sort((panelKeyA, panelKeyB) => {
|
||||
|
@ -81,28 +93,45 @@ export const resolveGridRow = (
|
|||
originalRowData: GridRowData,
|
||||
dragRequest?: GridPanelData
|
||||
): GridRowData => {
|
||||
const nextRowData = { ...originalRowData, panels: { ...originalRowData.panels } };
|
||||
|
||||
// Apply drag request
|
||||
let 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
|
||||
// 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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
// while the layout has at least one collision, try to resolve them in order
|
||||
let collision = getFirstCollision(nextRowData, sortedKeys);
|
||||
while (collision !== undefined) {
|
||||
nextRowData = resolvePanelCollisions(nextRowData, nextRowData.panels[collision], sortedKeys);
|
||||
collision = getFirstCollision(nextRowData, sortedKeys);
|
||||
}
|
||||
const compactedGrid = compactGridRow(nextRowData);
|
||||
return compactedGrid;
|
||||
return compactGridRow(nextRowData); // compact the grid to close any gaps
|
||||
};
|
||||
|
||||
/**
|
||||
* for each panel that collides with `panelToResolve`, push the colliding panel down by a single row and
|
||||
* recursively handle any collisions that result from that move
|
||||
*/
|
||||
function resolvePanelCollisions(
|
||||
rowData: GridRowData,
|
||||
panelToResolve: GridPanelData,
|
||||
keysInOrder: string[]
|
||||
): GridRowData {
|
||||
const collisions = getAllCollisionsWithPanel(panelToResolve, rowData, keysInOrder);
|
||||
for (const collision of collisions) {
|
||||
if (collision.id === panelToResolve.id) continue;
|
||||
rowData.panels[collision.id].row++;
|
||||
rowData = resolvePanelCollisions(
|
||||
rowData,
|
||||
rowData.panels[collision.id],
|
||||
/**
|
||||
* when recursively resolving any collisions that result from moving this colliding panel down,
|
||||
* ignore if `collision` is still colliding with `panelToResolve` to prevent an infinite loop
|
||||
*/
|
||||
keysInOrder.filter((key) => key !== panelToResolve.id)
|
||||
);
|
||||
}
|
||||
return rowData;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue