Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dashboard][Collapsable Panels] New collision resolution algorithm #204134

Conversation

Heenawter
Copy link
Contributor

@Heenawter Heenawter commented Dec 12, 2024

Closes #191306

Summary

This PR updates the collision resolution algorithm to handle the edge case described in the linked issue.

In the old algorithm, we checked each panel for collisions a single time - however, this caused a bug where panels could get re-ordered depending on the order that we pushed panels down + how much distance we were pushing them down by. While this algorithm was extremely efficient (approximately $O \left({ { n^2 + n } \over 2 }\right)$ before compaction where n is the number of panels), it was not as simple as "fixing" the bug because, no matter what, there was always an edge case that could not be handled without adding some sort of backtracking - instead, we had to come up with a new algorithm.

Example of Edge Case

To better understand how the old and new algorithms compare (and how they work), consider a scenario where the grid layout has 8 columns and the panels are laid out as follows:

image

Now, what happens when panel 4 (yellow) is dragged to the left so that it overlaps both panel 3 (red) and panel 4 (blue)?

Old Algorithm

With the old algorithm, we get the following behaviour:

Dec-16-2024 17-04-58

This is because we are only pushing the collided panels down once. So, to describe what happens in detail, we start by gathering up the panels in order (top-bottom, left-right, where dragged panels take priority when rows are equal) - this results in an order of [panel4, panel3, panel1, panel2].

We start checking for collisions in this order - starting with panel 4 (yellow), we determine that it is colliding with panel 3 (red) and panel 2 (blue). We start with panel 3 (since we are once again doing this "in order"), and we push it down by 5 rows (since this is the overlap of the panel 3 and panel 4). Then, to "resolve" the second collision with panel 4, we push down panel 2 by 3 rows. This leaves the layout in the following state:

image

Now, since panel 4 (yellow) is "resolved", we move on to the next panel in the [panel4, panel3, panel1, panel2] order - panel 3 (red). Panel 3 now collides with panel 2, so we push panel 2 down by two rows. This is the only collision with panel 3, so we check panel 1 and panel 2, neither of which report collisions at this point. So the layout ends in the following state:

image

So, what's the problem here? Well, panel 1 (green) is now "out of sync" with the old panel ordering because it got "left behind" in the collision resolution. Instead, we should have moved panel 1 by the same amount as panel 2.

New Algorithm

With the new algorithm, we get the expected behaviour:

Dec-16-2024 17-30-28

If we run through the algorithm, we identify that the first collision in the layout (based on left to right, top to bottom ordering, with priority being the panel being dragged) is panel 4 (yellow) - so, we enter the while loop and recurse to resolve all collisions with panel 4 via resolvePanelCollisions. The recursion within resolvePanelCollisions works as follows - get all collisions with panel 4 (which, at this point, returns panel 3 (red) and panel 2 (blue)) and push them down by a single row, then recurse on those panels to resolve any of the resulting collisions.

It's difficult to describe recursion, but let me try my best :) During the collision resolution on panel 4, we start by pushing panel 3 down and then recursively resolving the collisions on panel 3. Since pushing panel 3 causes collisions with panel 1 and 2 (in order), we push them both down by one row - starting with panel 1. We recurse to resolve the collisions on panel 1, which has no collisions - then we do the same for panel 2, which also reports no collisions. The collisions with panel 3 are "resolved" (i.e. we can "back out" of the recursion on panels 1 and 2), so we go back to resolving panel 4, push panel 2 down, and then resolve the collisions on panel 2 - which reports no collisions. In summary:

image

Therefore, after the first iteration of the while loop, this results in the following layout, where panel 3 (red) has been pushed down by one row, panel 1 (green) has been pushed down by one row, and panel 2 (blue) has been pushed down by two rows:

image

We then return to the while loop and check if the layout has any other collisions - and panel 4 is still our "first" collision! So we repeat the process above and push both panel 4 and panel 2 down and recurse to resolve the resulting collisions on those panels. This continues as follows:

Steps Resulting Layout

And so, after our fifth iteration of the while loop, the layout is no longer reporting collisions - so, our layout is now fully resolved!

Performance Comparison

This algorithm is more-or-less the same as the algorithm used in react-grid-layout - however, instead of running on every frame, our layout engine only resolves collisions as they happen (hence why the while loop is necessary). This results in much better rendering performance.

To test this, I opened the Logs sample dashboard in both the example app (which is obviously using kbn-grid-layout) and Dashboard (which is still using react-grid-layout) and performed various drag actions on both while recording their performance. For example, consider the following capture of a drag that doesn't cause any collisions:

kbn-grid-layout react-grid-layout
image image

You can see that, even with no collisions, the performance is slightly better with kbn-grid-layout because we don't ever recurse on collisions in this scenario. But it gets even better when actual collisions happen - for example, consider the performance when the dragged panel only causes a single collision:

kbn-grid-layout react-grid-layout
image image

Versus when the dragged panel causes multiple collisions at the top of the dashboard that causes a chain reaction of panels needing to be pushed down:

kbn-grid-layout react-grid-layout
image image

In all of the above scenarios, performance is improved overall and there are fewer red "spikes" / bottlenecks due to forced reflow along the way. Most of the time saved is caused by significant reductions to both rendering and painting time in kbn-grid-layout when compared to react-grid-layout. While some of this improvement may be explained by differences in the example grid plugin versus the Dashboard plugin, this is enough proof that performance of kbn-grid-layout is at least as good as react-grid-layout and so swapping out the layout engine should not cause any performance regressions.

Other Notable Algorithm Attempts

I tried a few drag/resize event specific algorithms that only work when an interaction event is happening - i.e. they rely on information from the panel that is being interacted with in order to function.

Note that, if we decided to go this route, we would still need some generic collision resolution algorithm (such as the one proposed in this PR) that we could use to resolve a layout when no interaction event is happening. After all, we cannot assume that the grid layout that is sent as a prop is valid (i.e. it could have overlapping panels at the start, before any dragging and/or resizing has occurred) and we need a way to verify + resolve it.

Unfortunately, both of the "interaction-event necessary" collision resolution algorithms that I tried suffered from bugs so, while it might be possible to make a more efficient algorithm for this, I ultimately threw them away in favour of using the "generic" collision resolution algorithm proposed in this PR.

That being said, I figured it was worth documenting the things that I tried and why they failed.

Algorithm 1

I tried a modification of the original collision resolution algorithm where, instead of just moving the colliding panels down by the rowOverlap amount, we move all panels in or below the targeted grid row down by the height of the panel being dragged. However, due to the compaction algorithm ignoring all panels that have collisions, you can end up in an infinite "push panels down" loop like so:

Dec-16-2024 15-31-22

This is because panel 3 is always colliding with panel 1 and 4, so they never get pushed up to "close" the gaps. To try and get around this, I modified the collision detection used for panel compaction so that collisions only "stop" compaction if the collision occurs on a panel above the given panel we are trying to compact. However, this caused a different bug:

Dec-16-2024 15-12-22

In the above GIF, consider what happens when panel 3 (red) targets row 3. As expected, we move all other panels (1, 2, and 4) down by a single row. Then, when we reach the compaction step, we start by trying to push panel 1 up (since it is now the top-left-most panel) - and, since we are filtering out the collision with panel 3 (since it occurs "below" panel 1), it gets pushed all the way to the top. Then, we push panel 4 all the way to the top (because it is also ignoring the collision with panel 3), followed by pushing panel 2 up until it would collide with panel 3. Unfortunately though, we didn't account for the full height of panel 4 - so panel 3 is now stuck colliding with panel 4.

I modified this algorithm further to move the panels down by the "row overlap" with the dragged panel rather than the dragged panel's height - but this also suffers from a bug:

Dec-16-2024 16-29-12

At which point, I abandoned this approach becase it felt like there were too many edge cases that could not all be accounted for. It's possible we could explore this further but, given the limitations of this algorithm only working for resolving collisions given an interaction event, I opted to move on.

Algorithm 2

I tried an algorithm where, similar to the algorithm proposed in this PR, I recorded the panel IDs in each cell that a given panel occupied - however, instead of moving things down row by row and handling the resulting collisions, we moved every panel that was below the targetted row down by the height of the panel being dragged. In theory, the compaction algorithm would then take control of closing all the resulting gaps.

Unfortunately, this didn't quite work in practice. Consider the following:

Dec-16-2024 10-17-21

According to the algorithm defined above, panel 2 (blue) and panel 4 (yellow) both need to move down when panel 3 (red) is dragged to target row 4 (remember that row zero is the first row), since they both occupy this row - so we push both of them down by 1 (the height of panel 3). However, once we start compacting the layout in order to finish our collision resolution, we hit a snag with panel 4 (yellow) - when we start trying to compact panel 4, getAllCollisionsWithPanel returns that panel 3 is colliding with panel 4 and so we break out of the compaction - hence why panel 4 continues to float. This then has a snowball effect on all other panels as they report collisions and therefore do not get pushed up.

Algorithm 3

This was the algorithm we almost went with, because it works! But it suffered from two things - one, the code was complicated and difficult to maintain and two, the collision resolution behaviour felt less natural because it always favoured the dragged panel. This made it too sensitive to pushing items down; so even if a panel was targeting the bottom row of a different panel, it would push that panel down to make room for the dragged panel.

In the simplest terms, this algorithm worked as follows: after creating a 3D array representing the current grid (a 2D array of panel ID arrays), for each row, while there are collisions, push panels down in reverse order (bottom right to top left); continue until all rows have no collisions. This algorithm hd an efficiency of approximately $O\left(r * c \right)$ to set up the 3D array and $O \left( c * h * n \right)$ to resolve the conflicts, where r is the number of rows, c is the number of columns (minus 1), h is the height of the dragged panel, and n is the number of panels. This results in an overall speed of about $O \left( (r * c) + (c * h * n) \right)$

To better understand how this algorithm works, consider how it handles the edge case described above:

Algorithm 3 - Edge Case Example

Rather than checking every panel for collisions a single time, we check and resolve each row for collisions and resolve them. This gives the following behaviour:

Dec-16-2024 17-30-28

We start by creating a 3D array representing the grid - at the first point of collision, it looks like:

0 1 2 3 4 5 6 7
0 [panel3] [panel3] [panel3] [panel3, panel4] [panel4] [panel4] [panel4] []
1 [panel3] [panel3] [panel3] [panel3, panel4] [panel4] [panel4] [panel4] []
2 [panel1] [panel1] [panel2] [panel2, panel4] [panel4] [panel4] [panel4] []
3 [panel1] [panel1] [panel2] [panel2, panel4] [panel4] [panel4] [panel4] []
4 [panel1] [panel1] [panel2] [panel2, panel4] [panel4] [panel4] [panel4] []

Then, we start checking and resolving collisions, row by row. Starting with row 0, we see that column 3 has a collision between panel 3 and panel 4 - we ignore the panel being dragged (panel 4), so we push down panel 3 by one row. Row 0 has no more collisions, so we move on to row 1 and, following the same logic as above, we push down panel 3 again. This leaves the grid in the following state:

image

0 1 2 3 4 5 6 7
0 [] [] [] [panel4] [panel4] [panel4] [panel4] []
1 [] [] [] [panel4] [panel4] [panel4] [panel4] []
2 [panel1, panel3] [panel1, panel3] [panel2, panel3] [panel3, panel2, panel4] [panel4] [panel4] [panel4] []
3 [panel1, panel3] [panel1, panel3] [panel2, panel3] [panel3, panel2, panel4] [panel4] [panel4] [panel4] []
4 [panel1] [panel1] [panel2] [panel2, panel4] [panel4] [panel4] [panel4] []

Now, on row 2, we have collisions in columns zero through three, so we push the panels in reverse order (i.e from the bottom right to the top left) - this results in pushing panel 2 down, then panel 1, and finally panel 3. We have to add a row in order to accomplish this because otherwise we "lose" the bottom of panel 2, so our layout now looks like this and row 2 has no more collisions:

image

0 1 2 3 4 5 6 7
0 [] [] [] [panel4] [panel4] [panel4] [panel4] []
1 [] [] [] [panel4] [panel4] [panel4] [panel4] []
2 [] [] [] [panel4] [panel4] [panel4] [panel4] []
3 [panel1, panel3] [panel1, panel3] [panel2, panel3] [panel3, panel2, panel4] [panel4] [panel4] [panel4] []
4 [panel1, panel3] [panel1, panel3] [panel2, panel3] [panel3, panel2, panel4] [panel4] [panel4] [panel4] []
5 [panel1] [panel1] [panel2] [panel2] [] [] [] []

Continuing this behaviour, we push panel 2, panel 1, and panel 3 down in row 3; then again in row 4. Once we reach and resolve row 5 (i.e. get row 5 to the point where there are no collisions with panel 3), our layout looks like so:

image

0 1 2 3 4 5 6 7
0 [] [] [] [panel4] [panel4] [panel4] [panel4] []
1 [] [] [] [panel4] [panel4] [panel4] [panel4] []
2 [] [] [] [panel4] [panel4] [panel4] [panel4] []
3 [] [] [] [panel4] [panel4] [panel4] [panel4] []
4 [] [] [] [panel4] [panel4] [panel4] [panel4] []
5 [panel3] [panel3] [panel3] [panel3] [] [] [] []
6 [panel3, panel1] [panel3, panel1] [panel2, panel3] [panel2, panel3] [] [] [] []
7 [panel1] [panel1] [panel2] [panel2] [] [] [] []
8 [panel1] [panel1] [panel2] [panel2] [] [] [] []

At this point, all collisions on panel 4 have been resolved. So, in row 6, we see that panels 2 and 1 are now colliding with panel 3 - so, we push both of them down. At that point, row 6 does not have collisions so we don't push panel 3 down any further - and our layout is resolved!

image

0 1 2 3 4 5 6 7
0 [] [] [] [panel4] [panel4] [panel4] [panel4] []
1 [] [] [] [panel4] [panel4] [panel4] [panel4] []
2 [] [] [] [panel4] [panel4] [panel4] [panel4] []
3 [] [] [] [panel4] [panel4] [panel4] [panel4] []
4 [] [] [] [panel4] [panel4] [panel4] [panel4] []
5 [panel3] [panel3] [panel3] [panel3] [] [] [] []
6 [panel3] [panel3] [panel3] [panel3] [] [] [] []
7 [panel1] [panel1] [panel2] [panel2] [] [] [] []
8 [panel1] [panel1] [panel2] [panel2] [] [] [] []
9 [panel1] [panel1] [panel2] [panel2] [] [] [] []

Checklist

  • Unit or functional tests were updated or added to match the most common scenarios
  • The PR description includes the appropriate Release Notes section, and the correct release_note:* label is applied per the guidelines

Identify risks

There are no risks to this PR, since all work is contained in the examples plugin.

@Heenawter Heenawter added Feature:Dashboard Dashboard related features Team:Presentation Presentation Team for Dashboard, Input Controls, and Canvas loe:medium Medium Level of Effort release_note:skip Skip the PR/issue when compiling release notes impact:high Addressing this issue will have a high level of impact on the quality/strength of our product. backport:prev-minor Backport to (8.x) the previous minor version (i.e. one version back from main) labels Dec 12, 2024
@Heenawter Heenawter self-assigned this Dec 12, 2024
@Heenawter Heenawter force-pushed the kbn-grid-layout_new-collision-algorithm_2024-12-12 branch from 68849b3 to c176992 Compare December 12, 2024 21:34
@Heenawter Heenawter changed the title [Dashboard][Collapsable Panels] New collision detection algorithm [Dashboard][Collapsable Panels] New collision resolution algorithm Dec 16, 2024
Comment on lines +49 to +50
filters$: new BehaviorSubject([]),
query$: new BehaviorSubject(''),
Copy link
Contributor Author

@Heenawter Heenawter Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was added so that the Lens embeddables work - I think a recent merge broke them in this example 🤷

if (dragRequest) {
nextRowData.panels[dragRequest.id] = dragRequest;
}
// return nextRowData;
Copy link
Contributor Author

@Heenawter Heenawter Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this commented out line stuck around for so long 🤦 It's been there from the very first POC 🫠

@Heenawter Heenawter marked this pull request as ready for review December 18, 2024 19:23
@Heenawter Heenawter requested a review from a team as a code owner December 18, 2024 19:23
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-presentation (Team:Presentation)

@Heenawter Heenawter marked this pull request as draft December 19, 2024 22:12
@Heenawter Heenawter marked this pull request as ready for review December 30, 2024 19:47
@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

✅ unchanged

History

cc @Heenawter

Copy link
Contributor

@ThomThomson ThomThomson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new algorithm is extremely clean and efficient, and works great. I also tested this locally in the example plugin, and it feels good to use. Great work and LGTM! I also experimented with an alternative solution which I've described below:

Alternate Solution

This alternative comes with the following caveats:

  1. I'm not able to break it with any regular dragging, dropping or resizing, however I haven't fully tested this so there may be edge cases with this algorithm that I haven't thought through.
  2. This is not meant to be a replacement for this PR, and this PR should merge as-is. This is simply an alternative avenue we can look into down the road as a potential iterative improvement.

Alternate Solution explanation

I noticed that this edge case is caused by the collision resolution algorithm effectively leaving behind a panel that should have been moved. This panel (panel1 in the example) is left behind because it isn't technically colliding with the moved panel (panel4 in the example). My theory was that if we widened the criteria for panels to be considered colliding, that the edge case would disappear. To accomplish this I created a separate collides function that only takes rows into account i.e. a panel is considered colliding if it shares any rows in common.

const collidesOnRow = (panelA: GridPanelData, panelB: GridPanelData) => {
  if (panelA.id === panelB.id) return false; // same panel
  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; // rows overlap
};

const getAllCollisionsOnRow = (
  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 (collidesOnRow(panelToCheck, comparePanel)) {
      collidingPanels.push(comparePanel);
    }
  }
  return collidingPanels;
};

In the resolveGridRow the getAllCollisionsOnRow function is used, and when compacting, the original collides implementation is used. This fixes the edge case:

fix

How this works is: for each panel in order we find all panels that share a row with this panel and push all of those other panels down by this panel's height. This effectively spaces all the panels out, giving them each their own row.

Then the original compaction algorithm takes over, removing all of the empty space.

Pros

  • Each panel is only visited once.
  • It uses our original algorithm with a small modification and no recursion.
  • It may perform better because it lacks the while loop and the recursion.

Cons

  • I haven't yet tested all edge cases
  • It requires compaction to work properly (this is arguably not a problem, as we aren't planning to turn off compaction any time soon.)


import { resolveGridRow } from './resolve_grid_row';

describe('resolve grid row', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to have jest coverage here!

@Heenawter Heenawter merged commit 1ff181c into elastic:main Jan 2, 2025
8 checks passed
@Heenawter Heenawter deleted the kbn-grid-layout_new-collision-algorithm_2024-12-12 branch January 2, 2025 23:04
@kibanamachine
Copy link
Contributor

Starting backport for target branches: 8.x

https://github.com/elastic/kibana/actions/runs/12589981480

kibanamachine pushed a commit to kibanamachine/kibana that referenced this pull request Jan 2, 2025
…lastic#204134)

Closes elastic#191306

## Summary

This PR updates the collision resolution algorithm to handle the edge
case described in the linked issue.

In the old algorithm, we checked each panel for collisions a **single
time** - however, this caused a bug where panels could get re-ordered
depending on the order that we pushed panels down + how much distance we
were pushing them down by. While this algorithm was extremely efficient
(approximately $`O \left({ { n^2 + n } \over 2 }\right)`$ before
compaction where `n` is the number of panels), it was not as simple as
"fixing" the bug because, no matter what, there was always an edge case
that could not be handled without adding some sort of backtracking -
instead, we had to come up with a new algorithm.

### Example of Edge Case

To better understand how the old and new algorithms compare (and how
they work), consider a scenario where the grid layout has 8 columns and
the panels are laid out as follows:

![image](https://github.com/user-attachments/assets/94fbb8f6-b6b8-4f9e-af91-0128717cdffc)

Now, what happens when panel 4 (yellow) is dragged to the left so that
it overlaps both panel 3 (red) and panel 4 (blue)?

<details>
<summary><b>Old Algorithm</b></summary>

With the old algorithm, we get the following behaviour:

![Dec-16-2024
17-04-58](https://github.com/user-attachments/assets/9a920046-cf53-4dff-bfbe-6ed0aed05f77)

This is because we are **only** pushing the collided panels down
**once**. So, to describe what happens in detail, we start by gathering
up the panels **in order** (top-bottom, left-right, where dragged panels
take priority when rows are equal) - this results in an order of
`[panel4, panel3, panel1, panel2]`.

We start checking for collisions in this order - starting with panel 4
(yellow), we determine that it is colliding with panel 3 (red) and panel
2 (blue). We start with panel 3 (since we are once again doing this "in
order"), and we push it down by 5 rows (since this is the overlap of the
panel 3 and panel 4). Then, to "resolve" the second collision with panel
4, we push down panel 2 by 3 rows. This leaves the layout in the
following state:

![image](https://github.com/user-attachments/assets/47aa981e-9e4f-4c30-8570-abf6ba4a20ba)

Now, since panel 4 (yellow) is "resolved", we move on to the next panel
in the `[panel4, panel3, panel1, panel2]` order - panel 3 (red). Panel 3
now collides with panel 2, so we push panel 2 down by two rows. This is
the only collision with panel 3, so we check panel 1 and panel 2,
neither of which report collisions at this point. So the layout ends in
the following state:

![image](https://github.com/user-attachments/assets/4c5e3aca-399c-4075-9897-a5d2d73e9284)

So, what's the problem here? Well, panel 1 (green) is now "out of sync"
with the old panel ordering because it got "left behind" in the
collision resolution. Instead, we should have moved panel 1 by the same
amount as panel 2.

</details>

<details>
<summary><b>New Algorithm</b></summary>

With the new algorithm, we get the expected behaviour:

![Dec-16-2024
17-30-28](https://github.com/user-attachments/assets/4cd7d49d-138b-4b59-ac78-0ea2cbc86c3c)

If we run through the algorithm, we identify that the **first**
collision in the layout (based on left to right, top to bottom ordering,
with priority being the panel being dragged) is panel 4 (yellow) - so,
we enter the `while` loop and recurse to resolve all collisions with
panel 4 via `resolvePanelCollisions`. The recursion within
`resolvePanelCollisions` works as follows - get **all** collisions with
panel 4 (which, at this point, returns panel 3 (red) and panel 2 (blue))
and push them down by a single row, then recurse on **those** panels to
resolve any of the resulting collisions.

It's difficult to describe recursion, but let me try my best :) During
the collision resolution on panel 4, we start by pushing panel 3 down
and then recursively resolving the collisions on panel 3. Since pushing
panel 3 causes collisions with panel 1 and 2 (in order), we push them
both down by one row - starting with panel 1. We recurse to resolve the
collisions on panel 1, which has no collisions - then we do the same for
panel 2, which also reports no collisions. The collisions with panel 3
are "resolved" (i.e. we can "back out" of the recursion on panels 1 and
2), so we go back to resolving panel 4, push panel **2** down, and then
resolve the collisions on panel 2 - which reports no collisions. In
summary:

![image](https://github.com/user-attachments/assets/ca82ca05-673e-4bf1-b22f-e2aa1c7b6ba4)

Therefore, after the first iteration of the `while` loop, this results
in the following layout, where panel 3 (red) has been pushed down by one
row, panel 1 (green) has been pushed down by one row, and panel 2 (blue)
has been pushed down by two rows:

![image](https://github.com/user-attachments/assets/ec7c08a5-d0e9-46f7-bf3c-2eede7cef6fd)

We then return to the `while` loop and check if the layout has any other
collisions - and panel 4 is still our "first" collision! So we repeat
the process above and push both panel 4 and panel 2 down and recurse to
resolve the resulting collisions on those panels. This continues as
follows:

| Steps | Resulting Layout |
|--------|--------|
| <img width="400"
src="https://github.com/user-attachments/assets/d2691a1c-5f0b-4333-bfd4-3cc57e1a3098"/>
| <img width="400"
src="https://github.com/user-attachments/assets/85325df3-7fbf-46fa-af98-10bbd883cf8d"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/e31a400f-d0d1-408d-baec-efa708ad0c52"/>
| <img width="400"
src="https://github.com/user-attachments/assets/c81762d4-443b-451c-b8fc-dabaf4e32ba1"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/4292e4a9-4157-4d77-9b69-b0f6a07338ac"/>
| <img width="400"
src="https://github.com/user-attachments/assets/bdd2dad5-6531-4d56-b8c2-dfb121fb6b5b"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/6d194a89-3ed6-46a6-9dcc-2b3fa0de9942"/>
| <img width="400"
src="https://github.com/user-attachments/assets/7e0b0fb5-bacb-49ad-ac86-02665a779b59"/>
|

And so, after our fifth iteration of the `while` loop, the layout is
**no longer** reporting collisions - so, our layout is now fully
resolved!

</details>

### Performance Comparison

This algorithm is more-or-less the same as the algorithm used in
`react-grid-layout` - however, instead of running on **every frame**,
our layout engine only resolves collisions **as they happen** (hence why
the while loop is necessary). This results in much better rendering
performance.

To test this, I opened the Logs sample dashboard in both the example app
(which is obviously using `kbn-grid-layout`) and Dashboard (which is
still using `react-grid-layout`) and performed various drag actions on
both while recording their performance. For example, consider the
following capture of a drag that doesn't cause **any** collisions:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/7fa1e9bb-e3dd-4f99-b011-0a46e53f0e88)
|
![image](https://github.com/user-attachments/assets/05e86a48-769d-4bae-ba48-d2fb3bdde90e)
|

You can see that, even with no collisions, the performance is slightly
better with `kbn-grid-layout` because we don't **ever** recurse on
collisions in this scenario. But it gets even better when actual
collisions happen - for example, consider the performance when the
dragged panel only causes a **single** collision:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/5e39a616-aa96-43be-a316-cb8fc4258b88)
|
![image](https://github.com/user-attachments/assets/520ab5b6-7049-4028-9d8c-63972aff9e92)
|

Versus when the dragged panel causes multiple collisions at the top of
the dashboard that causes a chain reaction of panels needing to be
pushed down:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/4d6b15c6-181a-4ea0-a179-09702b50411a)
|
![image](https://github.com/user-attachments/assets/363ef360-3c4f-4327-8031-18aca3833bce)
|

In all of the above scenarios, performance is improved overall **and**
there are fewer red "spikes" / bottlenecks due to [forced
reflow](https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing?utm_source=devtools#avoid-forced-synchronous-layouts)
along the way. Most of the time saved is caused by significant
reductions to both `rendering` and `painting` time in `kbn-grid-layout`
when compared to `react-grid-layout`. While some of this improvement may
be explained by differences in the example grid plugin versus the
Dashboard plugin, this is enough proof that performance of
`kbn-grid-layout` is **at least as good as** `react-grid-layout` and so
swapping out the layout engine should not cause any performance
regressions.

### Other Notable Algorithm Attempts

I tried a few **drag/resize event specific** algorithms that **only
work** when an interaction event is happening - i.e. they rely on
information from the panel that is being interacted with in order to
function.

Note that, if we decided to go this route, we would still need some
generic collision resolution algorithm (such as the one proposed in this
PR) that we could use to resolve a layout when **no** interaction event
is happening. After all, we cannot assume that the grid layout that is
sent as a prop is valid (i.e. it **could** have overlapping panels **at
the start**, before any dragging and/or resizing has occurred) and we
need a way to verify + resolve it.

Unfortunately, both of the "interaction-event necessary" collision
resolution algorithms that I tried suffered from bugs so, while it might
be **possible** to make a more efficient algorithm for this, I
ultimately threw them away in favour of using the "generic" collision
resolution algorithm proposed in this PR.

That being said, I figured it was worth documenting the things that I
tried and why they failed.

<details>
<summary><b>Algorithm 1</b></summary>

I tried a modification of the **original** collision resolution
algorithm where, instead of **just** moving the colliding panels down by
the `rowOverlap` amount, we move **all** panels in or below the targeted
grid row down by the **height** of the panel being dragged. However, due
to the compaction algorithm ignoring **all** panels that have
collisions, you can end up in an infinite "push panels down" loop like
so:

![Dec-16-2024
15-31-22](https://github.com/user-attachments/assets/29364168-6bd5-4b62-839e-a63636af71f5)

This is because panel 3 is **always** colliding with panel 1 and 4, so
they never get pushed up to "close" the gaps. To try and get around
this, I modified the collision detection used for panel compaction so
that collisions only "stop" compaction if the collision occurs on a
panel **above** the given panel we are trying to compact. However, this
caused a **different** bug:

![Dec-16-2024
15-12-22](https://github.com/user-attachments/assets/34f23bfd-6a5d-4ff5-8421-10b7bab89551)

In the above GIF, consider what happens when panel 3 (red) targets row
3. As expected, we move all other panels (1, 2, and 4) down by a single
row. Then, when we reach the compaction step, we start by trying to push
panel 1 up (since it is now the top-left-most panel) - and, since we are
filtering out the collision with panel 3 (since it occurs "below" panel
1), it gets pushed all the way to the top. Then, we push panel 4 all the
way to the top (because it is also ignoring the collision with panel 3),
followed by pushing panel 2 up until it would collide with panel 3.
Unfortunately though, we didn't account for the full height of panel 4 -
so panel 3 is now stuck colliding with panel 4.

I modified this algorithm **further** to move the panels down by the
"row overlap" with the dragged panel rather than the dragged panel's
height - but this also suffers from a bug:

![Dec-16-2024
16-29-12](https://github.com/user-attachments/assets/950cb573-0caf-4c44-8f63-2fc465c2c497)

At which point, I abandoned this approach becase it felt like there were
too many edge cases that could not all be accounted for. It's possible
we could explore this further but, given the limitations of this
algorithm **only working** for resolving collisions given an interaction
event, I opted to move on.
</details>

<details>
<summary><b>Algorithm 2</b></summary>

I tried an algorithm where, similar to the algorithm proposed in this
PR, I recorded the panel IDs in each cell that a given panel occupied -
however, instead of moving things down row by row and handling the
resulting collisions, we moved **every** panel that was **below** the
targetted row down by the **height** of the panel being dragged. In
theory, the compaction algorithm would then take control of closing all
the resulting gaps.

Unfortunately, this didn't quite work in practice. Consider the
following:

![Dec-16-2024
10-17-21](https://github.com/user-attachments/assets/8157dabd-bb52-4089-9493-74cbdcf49e5e)

According to the algorithm defined above, panel 2 (blue) and panel 4
(yellow) both need to move down when panel 3 (red) is dragged to target
row 4 (remember that row **zero** is the first row), since they both
occupy this row - so we push both of them down by 1 (the height of panel
3). However, once we start compacting the layout in order to finish our
collision resolution, we hit a snag with panel 4 (yellow) - when we
start trying to compact panel 4, `getAllCollisionsWithPanel` returns
that **panel 3** is colliding with panel 4 and so we **break out** of
the compaction - hence why panel 4 continues to float. This then has a
snowball effect on all other panels as they report collisions and
therefore do not get pushed up.
</details>

<details>
<summary><b>Algorithm 3</b></summary>

This was the algorithm we **almost** went with, because it works! But it
suffered from two things - one, the code was complicated and difficult
to maintain and two, the collision resolution behaviour felt less
natural because it **always** favoured the dragged panel. This made it
too sensitive to pushing items down; so even if a panel was targeting
the bottom row of a different panel, it would push that panel down to
make room for the dragged panel.

In the simplest terms, this algorithm worked as follows: after creating
a 3D array representing the current grid (a 2D array of panel ID
arrays), for each row, while there are collisions, push panels down in
**reverse order** (bottom right to top left); continue until all rows
have no collisions. This algorithm hd an efficiency of approximately
$`O\left(r * c \right)`$ to set up the 3D array and $`O \left( c * h * n
\right)`$ to resolve the conflicts, where `r` is the number of rows, `c`
is the number of columns (minus 1), `h` is the height of the dragged
panel, and `n` is the number of panels. This results in an overall speed
of about $`O \left( (r * c) + (c * h * n) \right)`$

To better understand how this algorithm works, consider how it handles
the edge case described above:

<details>
<summary>Algorithm 3 - Edge Case Example</summary>

Rather than checking every **panel** for collisions a single time, we
check and resolve each **row** for collisions and resolve them. This
gives the following behaviour:

![Dec-16-2024
17-30-28](https://github.com/user-attachments/assets/60cf3347-707c-4bc2-99e5-0e042c4cbed3)

We start by creating a 3D array representing the grid - at the **first**
point of collision, it looks like:

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **1** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **2** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **3** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **4** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |

Then, we start checking and resolving collisions, row by row. Starting
with row 0, we see that column 3 has a collision between panel 3 and
panel 4 - we **ignore** the panel being dragged (panel 4), so we push
down panel 3 by one row. Row 0 has no more collisions, so we move on to
row 1 and, following the same logic as above, we push down panel 3
again. This leaves the grid in the following state:

![image](https://github.com/user-attachments/assets/47ee1f8c-5107-41eb-8309-527e6fd773be)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **3** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **4** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |

Now, on row 2, we have collisions in columns zero through three, so we
push the panels in **reverse order** (i.e from the bottom right to the
top left) - this results in pushing panel 2 down, then panel 1, and
finally panel 3. We have to add a row in order to accomplish this
because otherwise we "lose" the bottom of panel 2, so our layout now
looks like this and row 2 has no more collisions:

![image](https://github.com/user-attachments/assets/b5697592-cb0e-4fac-90c9-6f3ade5e9db9)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **4** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **5** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

Continuing this behaviour, we push panel 2, panel 1, and panel 3 down in
row 3; then again in row 4. Once we reach and resolve row 5 (i.e. get
row 5 to the point where there are no collisions with panel 3), our
layout looks like so:

![image](https://github.com/user-attachments/assets/e59de766-a547-499a-a181-7841179032c0)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **4** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **5** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **6** | `[panel3, panel1]` | `[panel3, panel1]` | `[panel2, panel3]` |
`[panel2, panel3]` | `[]` | `[]` | `[]` | `[]` |
| **7** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **8** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

At this point, all collisions on panel 4 have been resolved. So, in row
6, we see that panels 2 and 1 are now colliding with panel 3 - so, we
push both of them down. At that point, row 6 does not have collisions so
we **don't** push panel 3 down any further - and our layout is resolved!

![image](https://github.com/user-attachments/assets/71869176-473f-4a48-8d0f-863b855628c4)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **4** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **5** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **6** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **7** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **8** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **9** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

</details>

</details>

### 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)

### Identify risks

There are no risks to this PR, since all work is contained in the
`examples` plugin.

---------

Co-authored-by: kibanamachine <[email protected]>
(cherry picked from commit 1ff181c)
@kibanamachine
Copy link
Contributor

💔 All backports failed

Status Branch Result
8.x Could not create pull request: Validation Failed: {"resource":"Issue","code":"custom","field":"body","message":"body is too long (maximum is 65536 characters)"}

Manual backport

To create the backport manually run:

node scripts/backport --pr 204134

Questions ?

Please refer to the Backport tool documentation

Heenawter added a commit to Heenawter/kibana that referenced this pull request Jan 3, 2025
…lastic#204134)

Closes elastic#191306

## Summary

This PR updates the collision resolution algorithm to handle the edge
case described in the linked issue.

In the old algorithm, we checked each panel for collisions a **single
time** - however, this caused a bug where panels could get re-ordered
depending on the order that we pushed panels down + how much distance we
were pushing them down by. While this algorithm was extremely efficient
(approximately $`O \left({ { n^2 + n } \over 2 }\right)`$ before
compaction where `n` is the number of panels), it was not as simple as
"fixing" the bug because, no matter what, there was always an edge case
that could not be handled without adding some sort of backtracking -
instead, we had to come up with a new algorithm.

### Example of Edge Case

To better understand how the old and new algorithms compare (and how
they work), consider a scenario where the grid layout has 8 columns and
the panels are laid out as follows:

![image](https://github.com/user-attachments/assets/94fbb8f6-b6b8-4f9e-af91-0128717cdffc)

Now, what happens when panel 4 (yellow) is dragged to the left so that
it overlaps both panel 3 (red) and panel 4 (blue)?

<details>
<summary><b>Old Algorithm</b></summary>

With the old algorithm, we get the following behaviour:

![Dec-16-2024
17-04-58](https://github.com/user-attachments/assets/9a920046-cf53-4dff-bfbe-6ed0aed05f77)

This is because we are **only** pushing the collided panels down
**once**. So, to describe what happens in detail, we start by gathering
up the panels **in order** (top-bottom, left-right, where dragged panels
take priority when rows are equal) - this results in an order of
`[panel4, panel3, panel1, panel2]`.

We start checking for collisions in this order - starting with panel 4
(yellow), we determine that it is colliding with panel 3 (red) and panel
2 (blue). We start with panel 3 (since we are once again doing this "in
order"), and we push it down by 5 rows (since this is the overlap of the
panel 3 and panel 4). Then, to "resolve" the second collision with panel
4, we push down panel 2 by 3 rows. This leaves the layout in the
following state:

![image](https://github.com/user-attachments/assets/47aa981e-9e4f-4c30-8570-abf6ba4a20ba)

Now, since panel 4 (yellow) is "resolved", we move on to the next panel
in the `[panel4, panel3, panel1, panel2]` order - panel 3 (red). Panel 3
now collides with panel 2, so we push panel 2 down by two rows. This is
the only collision with panel 3, so we check panel 1 and panel 2,
neither of which report collisions at this point. So the layout ends in
the following state:

![image](https://github.com/user-attachments/assets/4c5e3aca-399c-4075-9897-a5d2d73e9284)

So, what's the problem here? Well, panel 1 (green) is now "out of sync"
with the old panel ordering because it got "left behind" in the
collision resolution. Instead, we should have moved panel 1 by the same
amount as panel 2.

</details>

<details>
<summary><b>New Algorithm</b></summary>

With the new algorithm, we get the expected behaviour:

![Dec-16-2024
17-30-28](https://github.com/user-attachments/assets/4cd7d49d-138b-4b59-ac78-0ea2cbc86c3c)

If we run through the algorithm, we identify that the **first**
collision in the layout (based on left to right, top to bottom ordering,
with priority being the panel being dragged) is panel 4 (yellow) - so,
we enter the `while` loop and recurse to resolve all collisions with
panel 4 via `resolvePanelCollisions`. The recursion within
`resolvePanelCollisions` works as follows - get **all** collisions with
panel 4 (which, at this point, returns panel 3 (red) and panel 2 (blue))
and push them down by a single row, then recurse on **those** panels to
resolve any of the resulting collisions.

It's difficult to describe recursion, but let me try my best :) During
the collision resolution on panel 4, we start by pushing panel 3 down
and then recursively resolving the collisions on panel 3. Since pushing
panel 3 causes collisions with panel 1 and 2 (in order), we push them
both down by one row - starting with panel 1. We recurse to resolve the
collisions on panel 1, which has no collisions - then we do the same for
panel 2, which also reports no collisions. The collisions with panel 3
are "resolved" (i.e. we can "back out" of the recursion on panels 1 and
2), so we go back to resolving panel 4, push panel **2** down, and then
resolve the collisions on panel 2 - which reports no collisions. In
summary:

![image](https://github.com/user-attachments/assets/ca82ca05-673e-4bf1-b22f-e2aa1c7b6ba4)

Therefore, after the first iteration of the `while` loop, this results
in the following layout, where panel 3 (red) has been pushed down by one
row, panel 1 (green) has been pushed down by one row, and panel 2 (blue)
has been pushed down by two rows:

![image](https://github.com/user-attachments/assets/ec7c08a5-d0e9-46f7-bf3c-2eede7cef6fd)

We then return to the `while` loop and check if the layout has any other
collisions - and panel 4 is still our "first" collision! So we repeat
the process above and push both panel 4 and panel 2 down and recurse to
resolve the resulting collisions on those panels. This continues as
follows:

| Steps | Resulting Layout |
|--------|--------|
| <img width="400"
src="https://github.com/user-attachments/assets/d2691a1c-5f0b-4333-bfd4-3cc57e1a3098"/>
| <img width="400"
src="https://github.com/user-attachments/assets/85325df3-7fbf-46fa-af98-10bbd883cf8d"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/e31a400f-d0d1-408d-baec-efa708ad0c52"/>
| <img width="400"
src="https://github.com/user-attachments/assets/c81762d4-443b-451c-b8fc-dabaf4e32ba1"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/4292e4a9-4157-4d77-9b69-b0f6a07338ac"/>
| <img width="400"
src="https://github.com/user-attachments/assets/bdd2dad5-6531-4d56-b8c2-dfb121fb6b5b"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/6d194a89-3ed6-46a6-9dcc-2b3fa0de9942"/>
| <img width="400"
src="https://github.com/user-attachments/assets/7e0b0fb5-bacb-49ad-ac86-02665a779b59"/>
|

And so, after our fifth iteration of the `while` loop, the layout is
**no longer** reporting collisions - so, our layout is now fully
resolved!

</details>

### Performance Comparison

This algorithm is more-or-less the same as the algorithm used in
`react-grid-layout` - however, instead of running on **every frame**,
our layout engine only resolves collisions **as they happen** (hence why
the while loop is necessary). This results in much better rendering
performance.

To test this, I opened the Logs sample dashboard in both the example app
(which is obviously using `kbn-grid-layout`) and Dashboard (which is
still using `react-grid-layout`) and performed various drag actions on
both while recording their performance. For example, consider the
following capture of a drag that doesn't cause **any** collisions:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/7fa1e9bb-e3dd-4f99-b011-0a46e53f0e88)
|
![image](https://github.com/user-attachments/assets/05e86a48-769d-4bae-ba48-d2fb3bdde90e)
|

You can see that, even with no collisions, the performance is slightly
better with `kbn-grid-layout` because we don't **ever** recurse on
collisions in this scenario. But it gets even better when actual
collisions happen - for example, consider the performance when the
dragged panel only causes a **single** collision:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/5e39a616-aa96-43be-a316-cb8fc4258b88)
|
![image](https://github.com/user-attachments/assets/520ab5b6-7049-4028-9d8c-63972aff9e92)
|

Versus when the dragged panel causes multiple collisions at the top of
the dashboard that causes a chain reaction of panels needing to be
pushed down:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/4d6b15c6-181a-4ea0-a179-09702b50411a)
|
![image](https://github.com/user-attachments/assets/363ef360-3c4f-4327-8031-18aca3833bce)
|

In all of the above scenarios, performance is improved overall **and**
there are fewer red "spikes" / bottlenecks due to [forced
reflow](https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing?utm_source=devtools#avoid-forced-synchronous-layouts)
along the way. Most of the time saved is caused by significant
reductions to both `rendering` and `painting` time in `kbn-grid-layout`
when compared to `react-grid-layout`. While some of this improvement may
be explained by differences in the example grid plugin versus the
Dashboard plugin, this is enough proof that performance of
`kbn-grid-layout` is **at least as good as** `react-grid-layout` and so
swapping out the layout engine should not cause any performance
regressions.

### Other Notable Algorithm Attempts

I tried a few **drag/resize event specific** algorithms that **only
work** when an interaction event is happening - i.e. they rely on
information from the panel that is being interacted with in order to
function.

Note that, if we decided to go this route, we would still need some
generic collision resolution algorithm (such as the one proposed in this
PR) that we could use to resolve a layout when **no** interaction event
is happening. After all, we cannot assume that the grid layout that is
sent as a prop is valid (i.e. it **could** have overlapping panels **at
the start**, before any dragging and/or resizing has occurred) and we
need a way to verify + resolve it.

Unfortunately, both of the "interaction-event necessary" collision
resolution algorithms that I tried suffered from bugs so, while it might
be **possible** to make a more efficient algorithm for this, I
ultimately threw them away in favour of using the "generic" collision
resolution algorithm proposed in this PR.

That being said, I figured it was worth documenting the things that I
tried and why they failed.

<details>
<summary><b>Algorithm 1</b></summary>

I tried a modification of the **original** collision resolution
algorithm where, instead of **just** moving the colliding panels down by
the `rowOverlap` amount, we move **all** panels in or below the targeted
grid row down by the **height** of the panel being dragged. However, due
to the compaction algorithm ignoring **all** panels that have
collisions, you can end up in an infinite "push panels down" loop like
so:

![Dec-16-2024
15-31-22](https://github.com/user-attachments/assets/29364168-6bd5-4b62-839e-a63636af71f5)

This is because panel 3 is **always** colliding with panel 1 and 4, so
they never get pushed up to "close" the gaps. To try and get around
this, I modified the collision detection used for panel compaction so
that collisions only "stop" compaction if the collision occurs on a
panel **above** the given panel we are trying to compact. However, this
caused a **different** bug:

![Dec-16-2024
15-12-22](https://github.com/user-attachments/assets/34f23bfd-6a5d-4ff5-8421-10b7bab89551)

In the above GIF, consider what happens when panel 3 (red) targets row
3. As expected, we move all other panels (1, 2, and 4) down by a single
row. Then, when we reach the compaction step, we start by trying to push
panel 1 up (since it is now the top-left-most panel) - and, since we are
filtering out the collision with panel 3 (since it occurs "below" panel
1), it gets pushed all the way to the top. Then, we push panel 4 all the
way to the top (because it is also ignoring the collision with panel 3),
followed by pushing panel 2 up until it would collide with panel 3.
Unfortunately though, we didn't account for the full height of panel 4 -
so panel 3 is now stuck colliding with panel 4.

I modified this algorithm **further** to move the panels down by the
"row overlap" with the dragged panel rather than the dragged panel's
height - but this also suffers from a bug:

![Dec-16-2024
16-29-12](https://github.com/user-attachments/assets/950cb573-0caf-4c44-8f63-2fc465c2c497)

At which point, I abandoned this approach becase it felt like there were
too many edge cases that could not all be accounted for. It's possible
we could explore this further but, given the limitations of this
algorithm **only working** for resolving collisions given an interaction
event, I opted to move on.
</details>

<details>
<summary><b>Algorithm 2</b></summary>

I tried an algorithm where, similar to the algorithm proposed in this
PR, I recorded the panel IDs in each cell that a given panel occupied -
however, instead of moving things down row by row and handling the
resulting collisions, we moved **every** panel that was **below** the
targetted row down by the **height** of the panel being dragged. In
theory, the compaction algorithm would then take control of closing all
the resulting gaps.

Unfortunately, this didn't quite work in practice. Consider the
following:

![Dec-16-2024
10-17-21](https://github.com/user-attachments/assets/8157dabd-bb52-4089-9493-74cbdcf49e5e)

According to the algorithm defined above, panel 2 (blue) and panel 4
(yellow) both need to move down when panel 3 (red) is dragged to target
row 4 (remember that row **zero** is the first row), since they both
occupy this row - so we push both of them down by 1 (the height of panel
3). However, once we start compacting the layout in order to finish our
collision resolution, we hit a snag with panel 4 (yellow) - when we
start trying to compact panel 4, `getAllCollisionsWithPanel` returns
that **panel 3** is colliding with panel 4 and so we **break out** of
the compaction - hence why panel 4 continues to float. This then has a
snowball effect on all other panels as they report collisions and
therefore do not get pushed up.
</details>

<details>
<summary><b>Algorithm 3</b></summary>

This was the algorithm we **almost** went with, because it works! But it
suffered from two things - one, the code was complicated and difficult
to maintain and two, the collision resolution behaviour felt less
natural because it **always** favoured the dragged panel. This made it
too sensitive to pushing items down; so even if a panel was targeting
the bottom row of a different panel, it would push that panel down to
make room for the dragged panel.

In the simplest terms, this algorithm worked as follows: after creating
a 3D array representing the current grid (a 2D array of panel ID
arrays), for each row, while there are collisions, push panels down in
**reverse order** (bottom right to top left); continue until all rows
have no collisions. This algorithm hd an efficiency of approximately
$`O\left(r * c \right)`$ to set up the 3D array and $`O \left( c * h * n
\right)`$ to resolve the conflicts, where `r` is the number of rows, `c`
is the number of columns (minus 1), `h` is the height of the dragged
panel, and `n` is the number of panels. This results in an overall speed
of about $`O \left( (r * c) + (c * h * n) \right)`$

To better understand how this algorithm works, consider how it handles
the edge case described above:

<details>
<summary>Algorithm 3 - Edge Case Example</summary>

Rather than checking every **panel** for collisions a single time, we
check and resolve each **row** for collisions and resolve them. This
gives the following behaviour:

![Dec-16-2024
17-30-28](https://github.com/user-attachments/assets/60cf3347-707c-4bc2-99e5-0e042c4cbed3)

We start by creating a 3D array representing the grid - at the **first**
point of collision, it looks like:

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **1** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **2** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **3** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **4** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |

Then, we start checking and resolving collisions, row by row. Starting
with row 0, we see that column 3 has a collision between panel 3 and
panel 4 - we **ignore** the panel being dragged (panel 4), so we push
down panel 3 by one row. Row 0 has no more collisions, so we move on to
row 1 and, following the same logic as above, we push down panel 3
again. This leaves the grid in the following state:

![image](https://github.com/user-attachments/assets/47ee1f8c-5107-41eb-8309-527e6fd773be)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **3** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **4** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |

Now, on row 2, we have collisions in columns zero through three, so we
push the panels in **reverse order** (i.e from the bottom right to the
top left) - this results in pushing panel 2 down, then panel 1, and
finally panel 3. We have to add a row in order to accomplish this
because otherwise we "lose" the bottom of panel 2, so our layout now
looks like this and row 2 has no more collisions:

![image](https://github.com/user-attachments/assets/b5697592-cb0e-4fac-90c9-6f3ade5e9db9)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **4** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **5** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

Continuing this behaviour, we push panel 2, panel 1, and panel 3 down in
row 3; then again in row 4. Once we reach and resolve row 5 (i.e. get
row 5 to the point where there are no collisions with panel 3), our
layout looks like so:

![image](https://github.com/user-attachments/assets/e59de766-a547-499a-a181-7841179032c0)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **4** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **5** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **6** | `[panel3, panel1]` | `[panel3, panel1]` | `[panel2, panel3]` |
`[panel2, panel3]` | `[]` | `[]` | `[]` | `[]` |
| **7** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **8** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

At this point, all collisions on panel 4 have been resolved. So, in row
6, we see that panels 2 and 1 are now colliding with panel 3 - so, we
push both of them down. At that point, row 6 does not have collisions so
we **don't** push panel 3 down any further - and our layout is resolved!

![image](https://github.com/user-attachments/assets/71869176-473f-4a48-8d0f-863b855628c4)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **4** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **5** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **6** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **7** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **8** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **9** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

</details>

</details>

### 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)

### Identify risks

There are no risks to this PR, since all work is contained in the
`examples` plugin.

---------

Co-authored-by: kibanamachine <[email protected]>
(cherry picked from commit 1ff181c)
Heenawter added a commit to Heenawter/kibana that referenced this pull request Jan 3, 2025
…lastic#204134)

Closes elastic#191306

## Summary

This PR updates the collision resolution algorithm to handle the edge
case described in the linked issue.

In the old algorithm, we checked each panel for collisions a **single
time** - however, this caused a bug where panels could get re-ordered
depending on the order that we pushed panels down + how much distance we
were pushing them down by. While this algorithm was extremely efficient
(approximately $`O \left({ { n^2 + n } \over 2 }\right)`$ before
compaction where `n` is the number of panels), it was not as simple as
"fixing" the bug because, no matter what, there was always an edge case
that could not be handled without adding some sort of backtracking -
instead, we had to come up with a new algorithm.

### Example of Edge Case

To better understand how the old and new algorithms compare (and how
they work), consider a scenario where the grid layout has 8 columns and
the panels are laid out as follows:

![image](https://github.com/user-attachments/assets/94fbb8f6-b6b8-4f9e-af91-0128717cdffc)

Now, what happens when panel 4 (yellow) is dragged to the left so that
it overlaps both panel 3 (red) and panel 4 (blue)?

<details>
<summary><b>Old Algorithm</b></summary>

With the old algorithm, we get the following behaviour:

![Dec-16-2024
17-04-58](https://github.com/user-attachments/assets/9a920046-cf53-4dff-bfbe-6ed0aed05f77)

This is because we are **only** pushing the collided panels down
**once**. So, to describe what happens in detail, we start by gathering
up the panels **in order** (top-bottom, left-right, where dragged panels
take priority when rows are equal) - this results in an order of
`[panel4, panel3, panel1, panel2]`.

We start checking for collisions in this order - starting with panel 4
(yellow), we determine that it is colliding with panel 3 (red) and panel
2 (blue). We start with panel 3 (since we are once again doing this "in
order"), and we push it down by 5 rows (since this is the overlap of the
panel 3 and panel 4). Then, to "resolve" the second collision with panel
4, we push down panel 2 by 3 rows. This leaves the layout in the
following state:

![image](https://github.com/user-attachments/assets/47aa981e-9e4f-4c30-8570-abf6ba4a20ba)

Now, since panel 4 (yellow) is "resolved", we move on to the next panel
in the `[panel4, panel3, panel1, panel2]` order - panel 3 (red). Panel 3
now collides with panel 2, so we push panel 2 down by two rows. This is
the only collision with panel 3, so we check panel 1 and panel 2,
neither of which report collisions at this point. So the layout ends in
the following state:

![image](https://github.com/user-attachments/assets/4c5e3aca-399c-4075-9897-a5d2d73e9284)

So, what's the problem here? Well, panel 1 (green) is now "out of sync"
with the old panel ordering because it got "left behind" in the
collision resolution. Instead, we should have moved panel 1 by the same
amount as panel 2.

</details>

<details>
<summary><b>New Algorithm</b></summary>

With the new algorithm, we get the expected behaviour:

![Dec-16-2024
17-30-28](https://github.com/user-attachments/assets/4cd7d49d-138b-4b59-ac78-0ea2cbc86c3c)

If we run through the algorithm, we identify that the **first**
collision in the layout (based on left to right, top to bottom ordering,
with priority being the panel being dragged) is panel 4 (yellow) - so,
we enter the `while` loop and recurse to resolve all collisions with
panel 4 via `resolvePanelCollisions`. The recursion within
`resolvePanelCollisions` works as follows - get **all** collisions with
panel 4 (which, at this point, returns panel 3 (red) and panel 2 (blue))
and push them down by a single row, then recurse on **those** panels to
resolve any of the resulting collisions.

It's difficult to describe recursion, but let me try my best :) During
the collision resolution on panel 4, we start by pushing panel 3 down
and then recursively resolving the collisions on panel 3. Since pushing
panel 3 causes collisions with panel 1 and 2 (in order), we push them
both down by one row - starting with panel 1. We recurse to resolve the
collisions on panel 1, which has no collisions - then we do the same for
panel 2, which also reports no collisions. The collisions with panel 3
are "resolved" (i.e. we can "back out" of the recursion on panels 1 and
2), so we go back to resolving panel 4, push panel **2** down, and then
resolve the collisions on panel 2 - which reports no collisions. In
summary:

![image](https://github.com/user-attachments/assets/ca82ca05-673e-4bf1-b22f-e2aa1c7b6ba4)

Therefore, after the first iteration of the `while` loop, this results
in the following layout, where panel 3 (red) has been pushed down by one
row, panel 1 (green) has been pushed down by one row, and panel 2 (blue)
has been pushed down by two rows:

![image](https://github.com/user-attachments/assets/ec7c08a5-d0e9-46f7-bf3c-2eede7cef6fd)

We then return to the `while` loop and check if the layout has any other
collisions - and panel 4 is still our "first" collision! So we repeat
the process above and push both panel 4 and panel 2 down and recurse to
resolve the resulting collisions on those panels. This continues as
follows:

| Steps | Resulting Layout |
|--------|--------|
| <img width="400"
src="https://github.com/user-attachments/assets/d2691a1c-5f0b-4333-bfd4-3cc57e1a3098"/>
| <img width="400"
src="https://github.com/user-attachments/assets/85325df3-7fbf-46fa-af98-10bbd883cf8d"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/e31a400f-d0d1-408d-baec-efa708ad0c52"/>
| <img width="400"
src="https://github.com/user-attachments/assets/c81762d4-443b-451c-b8fc-dabaf4e32ba1"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/4292e4a9-4157-4d77-9b69-b0f6a07338ac"/>
| <img width="400"
src="https://github.com/user-attachments/assets/bdd2dad5-6531-4d56-b8c2-dfb121fb6b5b"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/6d194a89-3ed6-46a6-9dcc-2b3fa0de9942"/>
| <img width="400"
src="https://github.com/user-attachments/assets/7e0b0fb5-bacb-49ad-ac86-02665a779b59"/>
|

And so, after our fifth iteration of the `while` loop, the layout is
**no longer** reporting collisions - so, our layout is now fully
resolved!

</details>

### Performance Comparison

This algorithm is more-or-less the same as the algorithm used in
`react-grid-layout` - however, instead of running on **every frame**,
our layout engine only resolves collisions **as they happen** (hence why
the while loop is necessary). This results in much better rendering
performance.

To test this, I opened the Logs sample dashboard in both the example app
(which is obviously using `kbn-grid-layout`) and Dashboard (which is
still using `react-grid-layout`) and performed various drag actions on
both while recording their performance. For example, consider the
following capture of a drag that doesn't cause **any** collisions:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/7fa1e9bb-e3dd-4f99-b011-0a46e53f0e88)
|
![image](https://github.com/user-attachments/assets/05e86a48-769d-4bae-ba48-d2fb3bdde90e)
|

You can see that, even with no collisions, the performance is slightly
better with `kbn-grid-layout` because we don't **ever** recurse on
collisions in this scenario. But it gets even better when actual
collisions happen - for example, consider the performance when the
dragged panel only causes a **single** collision:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/5e39a616-aa96-43be-a316-cb8fc4258b88)
|
![image](https://github.com/user-attachments/assets/520ab5b6-7049-4028-9d8c-63972aff9e92)
|

Versus when the dragged panel causes multiple collisions at the top of
the dashboard that causes a chain reaction of panels needing to be
pushed down:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/4d6b15c6-181a-4ea0-a179-09702b50411a)
|
![image](https://github.com/user-attachments/assets/363ef360-3c4f-4327-8031-18aca3833bce)
|

In all of the above scenarios, performance is improved overall **and**
there are fewer red "spikes" / bottlenecks due to [forced
reflow](https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing?utm_source=devtools#avoid-forced-synchronous-layouts)
along the way. Most of the time saved is caused by significant
reductions to both `rendering` and `painting` time in `kbn-grid-layout`
when compared to `react-grid-layout`. While some of this improvement may
be explained by differences in the example grid plugin versus the
Dashboard plugin, this is enough proof that performance of
`kbn-grid-layout` is **at least as good as** `react-grid-layout` and so
swapping out the layout engine should not cause any performance
regressions.

### Other Notable Algorithm Attempts

I tried a few **drag/resize event specific** algorithms that **only
work** when an interaction event is happening - i.e. they rely on
information from the panel that is being interacted with in order to
function.

Note that, if we decided to go this route, we would still need some
generic collision resolution algorithm (such as the one proposed in this
PR) that we could use to resolve a layout when **no** interaction event
is happening. After all, we cannot assume that the grid layout that is
sent as a prop is valid (i.e. it **could** have overlapping panels **at
the start**, before any dragging and/or resizing has occurred) and we
need a way to verify + resolve it.

Unfortunately, both of the "interaction-event necessary" collision
resolution algorithms that I tried suffered from bugs so, while it might
be **possible** to make a more efficient algorithm for this, I
ultimately threw them away in favour of using the "generic" collision
resolution algorithm proposed in this PR.

That being said, I figured it was worth documenting the things that I
tried and why they failed.

<details>
<summary><b>Algorithm 1</b></summary>

I tried a modification of the **original** collision resolution
algorithm where, instead of **just** moving the colliding panels down by
the `rowOverlap` amount, we move **all** panels in or below the targeted
grid row down by the **height** of the panel being dragged. However, due
to the compaction algorithm ignoring **all** panels that have
collisions, you can end up in an infinite "push panels down" loop like
so:

![Dec-16-2024
15-31-22](https://github.com/user-attachments/assets/29364168-6bd5-4b62-839e-a63636af71f5)

This is because panel 3 is **always** colliding with panel 1 and 4, so
they never get pushed up to "close" the gaps. To try and get around
this, I modified the collision detection used for panel compaction so
that collisions only "stop" compaction if the collision occurs on a
panel **above** the given panel we are trying to compact. However, this
caused a **different** bug:

![Dec-16-2024
15-12-22](https://github.com/user-attachments/assets/34f23bfd-6a5d-4ff5-8421-10b7bab89551)

In the above GIF, consider what happens when panel 3 (red) targets row
3. As expected, we move all other panels (1, 2, and 4) down by a single
row. Then, when we reach the compaction step, we start by trying to push
panel 1 up (since it is now the top-left-most panel) - and, since we are
filtering out the collision with panel 3 (since it occurs "below" panel
1), it gets pushed all the way to the top. Then, we push panel 4 all the
way to the top (because it is also ignoring the collision with panel 3),
followed by pushing panel 2 up until it would collide with panel 3.
Unfortunately though, we didn't account for the full height of panel 4 -
so panel 3 is now stuck colliding with panel 4.

I modified this algorithm **further** to move the panels down by the
"row overlap" with the dragged panel rather than the dragged panel's
height - but this also suffers from a bug:

![Dec-16-2024
16-29-12](https://github.com/user-attachments/assets/950cb573-0caf-4c44-8f63-2fc465c2c497)

At which point, I abandoned this approach becase it felt like there were
too many edge cases that could not all be accounted for. It's possible
we could explore this further but, given the limitations of this
algorithm **only working** for resolving collisions given an interaction
event, I opted to move on.
</details>

<details>
<summary><b>Algorithm 2</b></summary>

I tried an algorithm where, similar to the algorithm proposed in this
PR, I recorded the panel IDs in each cell that a given panel occupied -
however, instead of moving things down row by row and handling the
resulting collisions, we moved **every** panel that was **below** the
targetted row down by the **height** of the panel being dragged. In
theory, the compaction algorithm would then take control of closing all
the resulting gaps.

Unfortunately, this didn't quite work in practice. Consider the
following:

![Dec-16-2024
10-17-21](https://github.com/user-attachments/assets/8157dabd-bb52-4089-9493-74cbdcf49e5e)

According to the algorithm defined above, panel 2 (blue) and panel 4
(yellow) both need to move down when panel 3 (red) is dragged to target
row 4 (remember that row **zero** is the first row), since they both
occupy this row - so we push both of them down by 1 (the height of panel
3). However, once we start compacting the layout in order to finish our
collision resolution, we hit a snag with panel 4 (yellow) - when we
start trying to compact panel 4, `getAllCollisionsWithPanel` returns
that **panel 3** is colliding with panel 4 and so we **break out** of
the compaction - hence why panel 4 continues to float. This then has a
snowball effect on all other panels as they report collisions and
therefore do not get pushed up.
</details>

<details>
<summary><b>Algorithm 3</b></summary>

This was the algorithm we **almost** went with, because it works! But it
suffered from two things - one, the code was complicated and difficult
to maintain and two, the collision resolution behaviour felt less
natural because it **always** favoured the dragged panel. This made it
too sensitive to pushing items down; so even if a panel was targeting
the bottom row of a different panel, it would push that panel down to
make room for the dragged panel.

In the simplest terms, this algorithm worked as follows: after creating
a 3D array representing the current grid (a 2D array of panel ID
arrays), for each row, while there are collisions, push panels down in
**reverse order** (bottom right to top left); continue until all rows
have no collisions. This algorithm hd an efficiency of approximately
$`O\left(r * c \right)`$ to set up the 3D array and $`O \left( c * h * n
\right)`$ to resolve the conflicts, where `r` is the number of rows, `c`
is the number of columns (minus 1), `h` is the height of the dragged
panel, and `n` is the number of panels. This results in an overall speed
of about $`O \left( (r * c) + (c * h * n) \right)`$

To better understand how this algorithm works, consider how it handles
the edge case described above:

<details>
<summary>Algorithm 3 - Edge Case Example</summary>

Rather than checking every **panel** for collisions a single time, we
check and resolve each **row** for collisions and resolve them. This
gives the following behaviour:

![Dec-16-2024
17-30-28](https://github.com/user-attachments/assets/60cf3347-707c-4bc2-99e5-0e042c4cbed3)

We start by creating a 3D array representing the grid - at the **first**
point of collision, it looks like:

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **1** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **2** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **3** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **4** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |

Then, we start checking and resolving collisions, row by row. Starting
with row 0, we see that column 3 has a collision between panel 3 and
panel 4 - we **ignore** the panel being dragged (panel 4), so we push
down panel 3 by one row. Row 0 has no more collisions, so we move on to
row 1 and, following the same logic as above, we push down panel 3
again. This leaves the grid in the following state:

![image](https://github.com/user-attachments/assets/47ee1f8c-5107-41eb-8309-527e6fd773be)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **3** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **4** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |

Now, on row 2, we have collisions in columns zero through three, so we
push the panels in **reverse order** (i.e from the bottom right to the
top left) - this results in pushing panel 2 down, then panel 1, and
finally panel 3. We have to add a row in order to accomplish this
because otherwise we "lose" the bottom of panel 2, so our layout now
looks like this and row 2 has no more collisions:

![image](https://github.com/user-attachments/assets/b5697592-cb0e-4fac-90c9-6f3ade5e9db9)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **4** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **5** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

Continuing this behaviour, we push panel 2, panel 1, and panel 3 down in
row 3; then again in row 4. Once we reach and resolve row 5 (i.e. get
row 5 to the point where there are no collisions with panel 3), our
layout looks like so:

![image](https://github.com/user-attachments/assets/e59de766-a547-499a-a181-7841179032c0)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **4** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **5** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **6** | `[panel3, panel1]` | `[panel3, panel1]` | `[panel2, panel3]` |
`[panel2, panel3]` | `[]` | `[]` | `[]` | `[]` |
| **7** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **8** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

At this point, all collisions on panel 4 have been resolved. So, in row
6, we see that panels 2 and 1 are now colliding with panel 3 - so, we
push both of them down. At that point, row 6 does not have collisions so
we **don't** push panel 3 down any further - and our layout is resolved!

![image](https://github.com/user-attachments/assets/71869176-473f-4a48-8d0f-863b855628c4)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **4** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **5** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **6** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **7** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **8** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **9** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

</details>

</details>

### 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)

### Identify risks

There are no risks to this PR, since all work is contained in the
`examples` plugin.

---------

Co-authored-by: kibanamachine <[email protected]>
(cherry picked from commit 1ff181c)
Heenawter added a commit that referenced this pull request Jan 3, 2025
…thm (#204134) (#205509)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Dashboard][Collapsable Panels] New collision resolution
algorithm](#204134)

<!--- Backport version: 8.9.8 -->
CAWilson94 pushed a commit to CAWilson94/kibana that referenced this pull request Jan 13, 2025
…lastic#204134)

Closes elastic#191306

## Summary

This PR updates the collision resolution algorithm to handle the edge
case described in the linked issue.

In the old algorithm, we checked each panel for collisions a **single
time** - however, this caused a bug where panels could get re-ordered
depending on the order that we pushed panels down + how much distance we
were pushing them down by. While this algorithm was extremely efficient
(approximately $`O \left({ { n^2 + n } \over 2 }\right)`$ before
compaction where `n` is the number of panels), it was not as simple as
"fixing" the bug because, no matter what, there was always an edge case
that could not be handled without adding some sort of backtracking -
instead, we had to come up with a new algorithm.

### Example of Edge Case

To better understand how the old and new algorithms compare (and how
they work), consider a scenario where the grid layout has 8 columns and
the panels are laid out as follows:


![image](https://github.com/user-attachments/assets/94fbb8f6-b6b8-4f9e-af91-0128717cdffc)

Now, what happens when panel 4 (yellow) is dragged to the left so that
it overlaps both panel 3 (red) and panel 4 (blue)?


<details>
<summary><b>Old Algorithm</b></summary>

With the old algorithm, we get the following behaviour:
  
![Dec-16-2024
17-04-58](https://github.com/user-attachments/assets/9a920046-cf53-4dff-bfbe-6ed0aed05f77)

This is because we are **only** pushing the collided panels down
**once**. So, to describe what happens in detail, we start by gathering
up the panels **in order** (top-bottom, left-right, where dragged panels
take priority when rows are equal) - this results in an order of
`[panel4, panel3, panel1, panel2]`.

We start checking for collisions in this order - starting with panel 4
(yellow), we determine that it is colliding with panel 3 (red) and panel
2 (blue). We start with panel 3 (since we are once again doing this "in
order"), and we push it down by 5 rows (since this is the overlap of the
panel 3 and panel 4). Then, to "resolve" the second collision with panel
4, we push down panel 2 by 3 rows. This leaves the layout in the
following state:

   

![image](https://github.com/user-attachments/assets/47aa981e-9e4f-4c30-8570-abf6ba4a20ba)

Now, since panel 4 (yellow) is "resolved", we move on to the next panel
in the `[panel4, panel3, panel1, panel2]` order - panel 3 (red). Panel 3
now collides with panel 2, so we push panel 2 down by two rows. This is
the only collision with panel 3, so we check panel 1 and panel 2,
neither of which report collisions at this point. So the layout ends in
the following state:


![image](https://github.com/user-attachments/assets/4c5e3aca-399c-4075-9897-a5d2d73e9284)

So, what's the problem here? Well, panel 1 (green) is now "out of sync"
with the old panel ordering because it got "left behind" in the
collision resolution. Instead, we should have moved panel 1 by the same
amount as panel 2.

</details>


<details>
<summary><b>New Algorithm</b></summary>

With the new algorithm, we get the expected behaviour:

![Dec-16-2024
17-30-28](https://github.com/user-attachments/assets/4cd7d49d-138b-4b59-ac78-0ea2cbc86c3c)

If we run through the algorithm, we identify that the **first**
collision in the layout (based on left to right, top to bottom ordering,
with priority being the panel being dragged) is panel 4 (yellow) - so,
we enter the `while` loop and recurse to resolve all collisions with
panel 4 via `resolvePanelCollisions`. The recursion within
`resolvePanelCollisions` works as follows - get **all** collisions with
panel 4 (which, at this point, returns panel 3 (red) and panel 2 (blue))
and push them down by a single row, then recurse on **those** panels to
resolve any of the resulting collisions.

It's difficult to describe recursion, but let me try my best :) During
the collision resolution on panel 4, we start by pushing panel 3 down
and then recursively resolving the collisions on panel 3. Since pushing
panel 3 causes collisions with panel 1 and 2 (in order), we push them
both down by one row - starting with panel 1. We recurse to resolve the
collisions on panel 1, which has no collisions - then we do the same for
panel 2, which also reports no collisions. The collisions with panel 3
are "resolved" (i.e. we can "back out" of the recursion on panels 1 and
2), so we go back to resolving panel 4, push panel **2** down, and then
resolve the collisions on panel 2 - which reports no collisions. In
summary:


![image](https://github.com/user-attachments/assets/ca82ca05-673e-4bf1-b22f-e2aa1c7b6ba4)


Therefore, after the first iteration of the `while` loop, this results
in the following layout, where panel 3 (red) has been pushed down by one
row, panel 1 (green) has been pushed down by one row, and panel 2 (blue)
has been pushed down by two rows:


![image](https://github.com/user-attachments/assets/ec7c08a5-d0e9-46f7-bf3c-2eede7cef6fd)

We then return to the `while` loop and check if the layout has any other
collisions - and panel 4 is still our "first" collision! So we repeat
the process above and push both panel 4 and panel 2 down and recurse to
resolve the resulting collisions on those panels. This continues as
follows:

| Steps | Resulting Layout |
|--------|--------|
| <img width="400"
src="https://github.com/user-attachments/assets/d2691a1c-5f0b-4333-bfd4-3cc57e1a3098"/>
| <img width="400"
src="https://github.com/user-attachments/assets/85325df3-7fbf-46fa-af98-10bbd883cf8d"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/e31a400f-d0d1-408d-baec-efa708ad0c52"/>
| <img width="400"
src="https://github.com/user-attachments/assets/c81762d4-443b-451c-b8fc-dabaf4e32ba1"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/4292e4a9-4157-4d77-9b69-b0f6a07338ac"/>
| <img width="400"
src="https://github.com/user-attachments/assets/bdd2dad5-6531-4d56-b8c2-dfb121fb6b5b"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/6d194a89-3ed6-46a6-9dcc-2b3fa0de9942"/>
| <img width="400"
src="https://github.com/user-attachments/assets/7e0b0fb5-bacb-49ad-ac86-02665a779b59"/>
|

And so, after our fifth iteration of the `while` loop, the layout is
**no longer** reporting collisions - so, our layout is now fully
resolved!

</details>

### Performance Comparison

This algorithm is more-or-less the same as the algorithm used in
`react-grid-layout` - however, instead of running on **every frame**,
our layout engine only resolves collisions **as they happen** (hence why
the while loop is necessary). This results in much better rendering
performance.

To test this, I opened the Logs sample dashboard in both the example app
(which is obviously using `kbn-grid-layout`) and Dashboard (which is
still using `react-grid-layout`) and performed various drag actions on
both while recording their performance. For example, consider the
following capture of a drag that doesn't cause **any** collisions:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/7fa1e9bb-e3dd-4f99-b011-0a46e53f0e88)
|
![image](https://github.com/user-attachments/assets/05e86a48-769d-4bae-ba48-d2fb3bdde90e)
|

You can see that, even with no collisions, the performance is slightly
better with `kbn-grid-layout` because we don't **ever** recurse on
collisions in this scenario. But it gets even better when actual
collisions happen - for example, consider the performance when the
dragged panel only causes a **single** collision:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------| 
|
![image](https://github.com/user-attachments/assets/5e39a616-aa96-43be-a316-cb8fc4258b88)
|
![image](https://github.com/user-attachments/assets/520ab5b6-7049-4028-9d8c-63972aff9e92)
|


Versus when the dragged panel causes multiple collisions at the top of
the dashboard that causes a chain reaction of panels needing to be
pushed down:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------| 
|
![image](https://github.com/user-attachments/assets/4d6b15c6-181a-4ea0-a179-09702b50411a)
|
![image](https://github.com/user-attachments/assets/363ef360-3c4f-4327-8031-18aca3833bce)
|

In all of the above scenarios, performance is improved overall **and**
there are fewer red "spikes" / bottlenecks due to [forced
reflow](https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing?utm_source=devtools#avoid-forced-synchronous-layouts)
along the way. Most of the time saved is caused by significant
reductions to both `rendering` and `painting` time in `kbn-grid-layout`
when compared to `react-grid-layout`. While some of this improvement may
be explained by differences in the example grid plugin versus the
Dashboard plugin, this is enough proof that performance of
`kbn-grid-layout` is **at least as good as** `react-grid-layout` and so
swapping out the layout engine should not cause any performance
regressions.


### Other Notable Algorithm Attempts

I tried a few **drag/resize event specific** algorithms that **only
work** when an interaction event is happening - i.e. they rely on
information from the panel that is being interacted with in order to
function.

Note that, if we decided to go this route, we would still need some
generic collision resolution algorithm (such as the one proposed in this
PR) that we could use to resolve a layout when **no** interaction event
is happening. After all, we cannot assume that the grid layout that is
sent as a prop is valid (i.e. it **could** have overlapping panels **at
the start**, before any dragging and/or resizing has occurred) and we
need a way to verify + resolve it.

Unfortunately, both of the "interaction-event necessary" collision
resolution algorithms that I tried suffered from bugs so, while it might
be **possible** to make a more efficient algorithm for this, I
ultimately threw them away in favour of using the "generic" collision
resolution algorithm proposed in this PR.

That being said, I figured it was worth documenting the things that I
tried and why they failed.

<details>
<summary><b>Algorithm 1</b></summary>

I tried a modification of the **original** collision resolution
algorithm where, instead of **just** moving the colliding panels down by
the `rowOverlap` amount, we move **all** panels in or below the targeted
grid row down by the **height** of the panel being dragged. However, due
to the compaction algorithm ignoring **all** panels that have
collisions, you can end up in an infinite "push panels down" loop like
so:
 
![Dec-16-2024
15-31-22](https://github.com/user-attachments/assets/29364168-6bd5-4b62-839e-a63636af71f5)

This is because panel 3 is **always** colliding with panel 1 and 4, so
they never get pushed up to "close" the gaps. To try and get around
this, I modified the collision detection used for panel compaction so
that collisions only "stop" compaction if the collision occurs on a
panel **above** the given panel we are trying to compact. However, this
caused a **different** bug:

![Dec-16-2024
15-12-22](https://github.com/user-attachments/assets/34f23bfd-6a5d-4ff5-8421-10b7bab89551)

In the above GIF, consider what happens when panel 3 (red) targets row
3. As expected, we move all other panels (1, 2, and 4) down by a single
row. Then, when we reach the compaction step, we start by trying to push
panel 1 up (since it is now the top-left-most panel) - and, since we are
filtering out the collision with panel 3 (since it occurs "below" panel
1), it gets pushed all the way to the top. Then, we push panel 4 all the
way to the top (because it is also ignoring the collision with panel 3),
followed by pushing panel 2 up until it would collide with panel 3.
Unfortunately though, we didn't account for the full height of panel 4 -
so panel 3 is now stuck colliding with panel 4.

I modified this algorithm **further** to move the panels down by the
"row overlap" with the dragged panel rather than the dragged panel's
height - but this also suffers from a bug:

![Dec-16-2024
16-29-12](https://github.com/user-attachments/assets/950cb573-0caf-4c44-8f63-2fc465c2c497)
   
At which point, I abandoned this approach becase it felt like there were
too many edge cases that could not all be accounted for. It's possible
we could explore this further but, given the limitations of this
algorithm **only working** for resolving collisions given an interaction
event, I opted to move on.
</details>

<details>
<summary><b>Algorithm 2</b></summary>

I tried an algorithm where, similar to the algorithm proposed in this
PR, I recorded the panel IDs in each cell that a given panel occupied -
however, instead of moving things down row by row and handling the
resulting collisions, we moved **every** panel that was **below** the
targetted row down by the **height** of the panel being dragged. In
theory, the compaction algorithm would then take control of closing all
the resulting gaps.
  
Unfortunately, this didn't quite work in practice. Consider the
following:
  
![Dec-16-2024
10-17-21](https://github.com/user-attachments/assets/8157dabd-bb52-4089-9493-74cbdcf49e5e)
 
  
According to the algorithm defined above, panel 2 (blue) and panel 4
(yellow) both need to move down when panel 3 (red) is dragged to target
row 4 (remember that row **zero** is the first row), since they both
occupy this row - so we push both of them down by 1 (the height of panel
3). However, once we start compacting the layout in order to finish our
collision resolution, we hit a snag with panel 4 (yellow) - when we
start trying to compact panel 4, `getAllCollisionsWithPanel` returns
that **panel 3** is colliding with panel 4 and so we **break out** of
the compaction - hence why panel 4 continues to float. This then has a
snowball effect on all other panels as they report collisions and
therefore do not get pushed up.
</details>
 

<details>
<summary><b>Algorithm 3</b></summary>

This was the algorithm we **almost** went with, because it works! But it
suffered from two things - one, the code was complicated and difficult
to maintain and two, the collision resolution behaviour felt less
natural because it **always** favoured the dragged panel. This made it
too sensitive to pushing items down; so even if a panel was targeting
the bottom row of a different panel, it would push that panel down to
make room for the dragged panel.

In the simplest terms, this algorithm worked as follows: after creating
a 3D array representing the current grid (a 2D array of panel ID
arrays), for each row, while there are collisions, push panels down in
**reverse order** (bottom right to top left); continue until all rows
have no collisions. This algorithm hd an efficiency of approximately
$`O\left(r * c \right)`$ to set up the 3D array and $`O \left( c * h * n
\right)`$ to resolve the conflicts, where `r` is the number of rows, `c`
is the number of columns (minus 1), `h` is the height of the dragged
panel, and `n` is the number of panels. This results in an overall speed
of about $`O \left( (r * c) + (c * h * n) \right)`$

To better understand how this algorithm works, consider how it handles
the edge case described above:


<details>
<summary>Algorithm 3 - Edge Case Example</summary>

Rather than checking every **panel** for collisions a single time, we
check and resolve each **row** for collisions and resolve them. This
gives the following behaviour:


![Dec-16-2024
17-30-28](https://github.com/user-attachments/assets/60cf3347-707c-4bc2-99e5-0e042c4cbed3)

We start by creating a 3D array representing the grid - at the **first**
point of collision, it looks like:

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **1** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **2** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **3** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **4** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |

Then, we start checking and resolving collisions, row by row. Starting
with row 0, we see that column 3 has a collision between panel 3 and
panel 4 - we **ignore** the panel being dragged (panel 4), so we push
down panel 3 by one row. Row 0 has no more collisions, so we move on to
row 1 and, following the same logic as above, we push down panel 3
again. This leaves the grid in the following state:


![image](https://github.com/user-attachments/assets/47ee1f8c-5107-41eb-8309-527e6fd773be)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **3** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **4** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |

Now, on row 2, we have collisions in columns zero through three, so we
push the panels in **reverse order** (i.e from the bottom right to the
top left) - this results in pushing panel 2 down, then panel 1, and
finally panel 3. We have to add a row in order to accomplish this
because otherwise we "lose" the bottom of panel 2, so our layout now
looks like this and row 2 has no more collisions:
   

![image](https://github.com/user-attachments/assets/b5697592-cb0e-4fac-90c9-6f3ade5e9db9)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **4** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **5** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

Continuing this behaviour, we push panel 2, panel 1, and panel 3 down in
row 3; then again in row 4. Once we reach and resolve row 5 (i.e. get
row 5 to the point where there are no collisions with panel 3), our
layout looks like so:


![image](https://github.com/user-attachments/assets/e59de766-a547-499a-a181-7841179032c0)
   
|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **4** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **5** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **6** | `[panel3, panel1]` | `[panel3, panel1]` | `[panel2, panel3]` |
`[panel2, panel3]` | `[]` | `[]` | `[]` | `[]` |
| **7** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **8** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

At this point, all collisions on panel 4 have been resolved. So, in row
6, we see that panels 2 and 1 are now colliding with panel 3 - so, we
push both of them down. At that point, row 6 does not have collisions so
we **don't** push panel 3 down any further - and our layout is resolved!
    

![image](https://github.com/user-attachments/assets/71869176-473f-4a48-8d0f-863b855628c4)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **4** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **5** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **6** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **7** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **8** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **9** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

</details>

</details>


### 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)

### Identify risks

There are no risks to this PR, since all work is contained in the
`examples` plugin.

---------

Co-authored-by: kibanamachine <[email protected]>
Heenawter added a commit that referenced this pull request Jan 14, 2025
…d-layout` (#205341)

Closes #190446

## Summary

This PR swaps out `react-grid-layout` for the new internal
`kbn-grid-layout` in the Dashboard plugin. This is the first major step
in making collapsible sections possible in Dashboard.

- **`react-grid-layout` (before)**:


https://github.com/user-attachments/assets/ca6ec059-7f4a-43fb-890e-7b72b781e50b

- **`kbn-grid-layout` (after)**:


https://github.com/user-attachments/assets/3d3de1f3-1afc-4e6b-93d6-9cc31a46e2cf

### Notable Improvements

- Better handling of resizing panels near the bottom of the screen
   
  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Jan-09-2025
09-59-00](https://github.com/user-attachments/assets/75854b76-3ad7-4f06-9745-b03bde15f87a)
| ![Jan-09-2025
09-26-24](https://github.com/user-attachments/assets/f0fbc0bf-9208-4866-b7eb-988c7abc3e50)
|


- Auto-scroll when dragging / resizing panels near the top and bottom of
the screen, making it much easier to move panels around by larger
distances

  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Jan-09-2025
10-01-30](https://github.com/user-attachments/assets/e3457e5e-3647-4024-b6e6-c594d6d3e1d7)
| ![Jan-09-2025
09-25-35](https://github.com/user-attachments/assets/3252bdec-2bbc-4793-b089-346866d4589b)
|

- More reliable panel positioning due to the use of CSS grid rather than
absolute positioning via pixels

  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Screenshot 2025-01-09 at 9 32
52 AM](https://github.com/user-attachments/assets/06bd31a4-0a9f-4561-84c3-4cd96ba297b0)
| ![Screenshot 2025-01-09 at 9 35
14 AM](https://github.com/user-attachments/assets/573dab98-3fb9-4ef6-9f37-c4cf4d03ce52)
|

- Better performance when dragging and resizing (see
#204134 for a more thorough
explanation) and a smaller bundle size than `react-grid-layout`

### 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)

### Identify risks

This PR contains a significant change to the Dashboard layout engine,
which means that it carries a decent amount of risk for introducing new,
uncaught bugs with dragging / resizing panels and collision resolution.
That being said, `kbn-grid-layout` has been built **iteratively** with
plenty of testing along the way to reduce this risk.

## Release note
Improves Dashboard layout engine by switching to the internally
developed `kbn-grid-layout`.

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Marta Bondyra <[email protected]>
Heenawter added a commit to Heenawter/kibana that referenced this pull request Jan 14, 2025
…d-layout` (elastic#205341)

Closes elastic#190446

## Summary

This PR swaps out `react-grid-layout` for the new internal
`kbn-grid-layout` in the Dashboard plugin. This is the first major step
in making collapsible sections possible in Dashboard.

- **`react-grid-layout` (before)**:

https://github.com/user-attachments/assets/ca6ec059-7f4a-43fb-890e-7b72b781e50b

- **`kbn-grid-layout` (after)**:

https://github.com/user-attachments/assets/3d3de1f3-1afc-4e6b-93d6-9cc31a46e2cf

### Notable Improvements

- Better handling of resizing panels near the bottom of the screen

  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Jan-09-2025
09-59-00](https://github.com/user-attachments/assets/75854b76-3ad7-4f06-9745-b03bde15f87a)
| ![Jan-09-2025
09-26-24](https://github.com/user-attachments/assets/f0fbc0bf-9208-4866-b7eb-988c7abc3e50)
|

- Auto-scroll when dragging / resizing panels near the top and bottom of
the screen, making it much easier to move panels around by larger
distances

  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Jan-09-2025
10-01-30](https://github.com/user-attachments/assets/e3457e5e-3647-4024-b6e6-c594d6d3e1d7)
| ![Jan-09-2025
09-25-35](https://github.com/user-attachments/assets/3252bdec-2bbc-4793-b089-346866d4589b)
|

- More reliable panel positioning due to the use of CSS grid rather than
absolute positioning via pixels

  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Screenshot 2025-01-09 at 9 32
52 AM](https://github.com/user-attachments/assets/06bd31a4-0a9f-4561-84c3-4cd96ba297b0)
| ![Screenshot 2025-01-09 at 9 35
14 AM](https://github.com/user-attachments/assets/573dab98-3fb9-4ef6-9f37-c4cf4d03ce52)
|

- Better performance when dragging and resizing (see
elastic#204134 for a more thorough
explanation) and a smaller bundle size than `react-grid-layout`

### 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)

### Identify risks

This PR contains a significant change to the Dashboard layout engine,
which means that it carries a decent amount of risk for introducing new,
uncaught bugs with dragging / resizing panels and collision resolution.
That being said, `kbn-grid-layout` has been built **iteratively** with
plenty of testing along the way to reduce this risk.

## Release note
Improves Dashboard layout engine by switching to the internally
developed `kbn-grid-layout`.

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Marta Bondyra <[email protected]>
(cherry picked from commit 6865715)

# Conflicts:
#	packages/kbn-grid-layout/grid/grid_panel/resize_handle.tsx
#	packages/kbn-grid-layout/grid/grid_row/grid_row.tsx
#	packages/kbn-grid-layout/grid/use_grid_layout_state.ts
Heenawter added a commit that referenced this pull request Jan 15, 2025
…bn-grid-layout` (#205341) (#206693)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Dashboard][Collapsable Panels] Swap `react-grid-layout` for
`kbn-grid-layout`
(#205341)](#205341)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Hannah
Mudge","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-01-14T21:51:14Z","message":"[Dashboard][Collapsable
Panels] Swap `react-grid-layout` for `kbn-grid-layout`
(#205341)\n\nCloses
https://github.com/elastic/kibana/issues/190446\r\n\r\n##
Summary\r\n\r\nThis PR swaps out `react-grid-layout` for the new
internal\r\n`kbn-grid-layout` in the Dashboard plugin. This is the first
major step\r\nin making collapsible sections possible in
Dashboard.\r\n\r\n- **`react-grid-layout`
(before)**:\r\n\r\n\r\nhttps://github.com/user-attachments/assets/ca6ec059-7f4a-43fb-890e-7b72b781e50b\r\n\r\n-
**`kbn-grid-layout`
(after)**:\r\n\r\n\r\nhttps://github.com/user-attachments/assets/3d3de1f3-1afc-4e6b-93d6-9cc31a46e2cf\r\n\r\n###
Notable Improvements\r\n\r\n- Better handling of resizing panels near
the bottom of the screen\r\n \r\n | `react-grid-layout` |
`kbn-grid-layout` |\r\n |--------|--------|\r\n|
![Jan-09-2025\r\n09-59-00](https://github.com/user-attachments/assets/75854b76-3ad7-4f06-9745-b03bde15f87a)\r\n|
![Jan-09-2025\r\n09-26-24](https://github.com/user-attachments/assets/f0fbc0bf-9208-4866-b7eb-988c7abc3e50)\r\n|\r\n\r\n\r\n-
Auto-scroll when dragging / resizing panels near the top and bottom
of\r\nthe screen, making it much easier to move panels around by
larger\r\ndistances\r\n\r\n | `react-grid-layout` | `kbn-grid-layout`
|\r\n |--------|--------|\r\n|
![Jan-09-2025\r\n10-01-30](https://github.com/user-attachments/assets/e3457e5e-3647-4024-b6e6-c594d6d3e1d7)\r\n|
![Jan-09-2025\r\n09-25-35](https://github.com/user-attachments/assets/3252bdec-2bbc-4793-b089-346866d4589b)\r\n|\r\n\r\n-
More reliable panel positioning due to the use of CSS grid rather
than\r\nabsolute positioning via pixels\r\n\r\n | `react-grid-layout` |
`kbn-grid-layout` |\r\n |--------|--------|\r\n| ![Screenshot 2025-01-09
at 9
32\r\n52 AM](https://github.com/user-attachments/assets/06bd31a4-0a9f-4561-84c3-4cd96ba297b0)\r\n|
![Screenshot 2025-01-09 at 9
35\r\n14 AM](https://github.com/user-attachments/assets/573dab98-3fb9-4ef6-9f37-c4cf4d03ce52)\r\n|\r\n\r\n-
Better performance when dragging and resizing
(see\r\nhttps://github.com//pull/204134 for a more
thorough\r\nexplanation) and a smaller bundle size than
`react-grid-layout`\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] The PR
description includes the appropriate Release Notes section,\r\nand the
correct `release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n###
Identify risks\r\n\r\nThis PR contains a significant change to the
Dashboard layout engine,\r\nwhich means that it carries a decent amount
of risk for introducing new,\r\nuncaught bugs with dragging / resizing
panels and collision resolution.\r\nThat being said, `kbn-grid-layout`
has been built **iteratively** with\r\nplenty of testing along the way
to reduce this risk.\r\n\r\n## Release note\r\nImproves Dashboard layout
engine by switching to the internally\r\ndeveloped
`kbn-grid-layout`.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>\r\nCo-authored-by:
Marta Bondyra
<[email protected]>","sha":"686571547f82f097d589776733e165466ec518ad","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Dashboard","Team:Presentation","loe:large","impact:critical","v9.0.0","release_note:feature","backport:prev-minor"],"number":205341,"url":"https://github.com/elastic/kibana/pull/205341","mergeCommit":{"message":"[Dashboard][Collapsable
Panels] Swap `react-grid-layout` for `kbn-grid-layout`
(#205341)\n\nCloses
https://github.com/elastic/kibana/issues/190446\r\n\r\n##
Summary\r\n\r\nThis PR swaps out `react-grid-layout` for the new
internal\r\n`kbn-grid-layout` in the Dashboard plugin. This is the first
major step\r\nin making collapsible sections possible in
Dashboard.\r\n\r\n- **`react-grid-layout`
(before)**:\r\n\r\n\r\nhttps://github.com/user-attachments/assets/ca6ec059-7f4a-43fb-890e-7b72b781e50b\r\n\r\n-
**`kbn-grid-layout`
(after)**:\r\n\r\n\r\nhttps://github.com/user-attachments/assets/3d3de1f3-1afc-4e6b-93d6-9cc31a46e2cf\r\n\r\n###
Notable Improvements\r\n\r\n- Better handling of resizing panels near
the bottom of the screen\r\n \r\n | `react-grid-layout` |
`kbn-grid-layout` |\r\n |--------|--------|\r\n|
![Jan-09-2025\r\n09-59-00](https://github.com/user-attachments/assets/75854b76-3ad7-4f06-9745-b03bde15f87a)\r\n|
![Jan-09-2025\r\n09-26-24](https://github.com/user-attachments/assets/f0fbc0bf-9208-4866-b7eb-988c7abc3e50)\r\n|\r\n\r\n\r\n-
Auto-scroll when dragging / resizing panels near the top and bottom
of\r\nthe screen, making it much easier to move panels around by
larger\r\ndistances\r\n\r\n | `react-grid-layout` | `kbn-grid-layout`
|\r\n |--------|--------|\r\n|
![Jan-09-2025\r\n10-01-30](https://github.com/user-attachments/assets/e3457e5e-3647-4024-b6e6-c594d6d3e1d7)\r\n|
![Jan-09-2025\r\n09-25-35](https://github.com/user-attachments/assets/3252bdec-2bbc-4793-b089-346866d4589b)\r\n|\r\n\r\n-
More reliable panel positioning due to the use of CSS grid rather
than\r\nabsolute positioning via pixels\r\n\r\n | `react-grid-layout` |
`kbn-grid-layout` |\r\n |--------|--------|\r\n| ![Screenshot 2025-01-09
at 9
32\r\n52 AM](https://github.com/user-attachments/assets/06bd31a4-0a9f-4561-84c3-4cd96ba297b0)\r\n|
![Screenshot 2025-01-09 at 9
35\r\n14 AM](https://github.com/user-attachments/assets/573dab98-3fb9-4ef6-9f37-c4cf4d03ce52)\r\n|\r\n\r\n-
Better performance when dragging and resizing
(see\r\nhttps://github.com//pull/204134 for a more
thorough\r\nexplanation) and a smaller bundle size than
`react-grid-layout`\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] The PR
description includes the appropriate Release Notes section,\r\nand the
correct `release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n###
Identify risks\r\n\r\nThis PR contains a significant change to the
Dashboard layout engine,\r\nwhich means that it carries a decent amount
of risk for introducing new,\r\nuncaught bugs with dragging / resizing
panels and collision resolution.\r\nThat being said, `kbn-grid-layout`
has been built **iteratively** with\r\nplenty of testing along the way
to reduce this risk.\r\n\r\n## Release note\r\nImproves Dashboard layout
engine by switching to the internally\r\ndeveloped
`kbn-grid-layout`.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>\r\nCo-authored-by:
Marta Bondyra
<[email protected]>","sha":"686571547f82f097d589776733e165466ec518ad"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/205341","number":205341,"mergeCommit":{"message":"[Dashboard][Collapsable
Panels] Swap `react-grid-layout` for `kbn-grid-layout`
(#205341)\n\nCloses
https://github.com/elastic/kibana/issues/190446\r\n\r\n##
Summary\r\n\r\nThis PR swaps out `react-grid-layout` for the new
internal\r\n`kbn-grid-layout` in the Dashboard plugin. This is the first
major step\r\nin making collapsible sections possible in
Dashboard.\r\n\r\n- **`react-grid-layout`
(before)**:\r\n\r\n\r\nhttps://github.com/user-attachments/assets/ca6ec059-7f4a-43fb-890e-7b72b781e50b\r\n\r\n-
**`kbn-grid-layout`
(after)**:\r\n\r\n\r\nhttps://github.com/user-attachments/assets/3d3de1f3-1afc-4e6b-93d6-9cc31a46e2cf\r\n\r\n###
Notable Improvements\r\n\r\n- Better handling of resizing panels near
the bottom of the screen\r\n \r\n | `react-grid-layout` |
`kbn-grid-layout` |\r\n |--------|--------|\r\n|
![Jan-09-2025\r\n09-59-00](https://github.com/user-attachments/assets/75854b76-3ad7-4f06-9745-b03bde15f87a)\r\n|
![Jan-09-2025\r\n09-26-24](https://github.com/user-attachments/assets/f0fbc0bf-9208-4866-b7eb-988c7abc3e50)\r\n|\r\n\r\n\r\n-
Auto-scroll when dragging / resizing panels near the top and bottom
of\r\nthe screen, making it much easier to move panels around by
larger\r\ndistances\r\n\r\n | `react-grid-layout` | `kbn-grid-layout`
|\r\n |--------|--------|\r\n|
![Jan-09-2025\r\n10-01-30](https://github.com/user-attachments/assets/e3457e5e-3647-4024-b6e6-c594d6d3e1d7)\r\n|
![Jan-09-2025\r\n09-25-35](https://github.com/user-attachments/assets/3252bdec-2bbc-4793-b089-346866d4589b)\r\n|\r\n\r\n-
More reliable panel positioning due to the use of CSS grid rather
than\r\nabsolute positioning via pixels\r\n\r\n | `react-grid-layout` |
`kbn-grid-layout` |\r\n |--------|--------|\r\n| ![Screenshot 2025-01-09
at 9
32\r\n52 AM](https://github.com/user-attachments/assets/06bd31a4-0a9f-4561-84c3-4cd96ba297b0)\r\n|
![Screenshot 2025-01-09 at 9
35\r\n14 AM](https://github.com/user-attachments/assets/573dab98-3fb9-4ef6-9f37-c4cf4d03ce52)\r\n|\r\n\r\n-
Better performance when dragging and resizing
(see\r\nhttps://github.com//pull/204134 for a more
thorough\r\nexplanation) and a smaller bundle size than
`react-grid-layout`\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] The PR
description includes the appropriate Release Notes section,\r\nand the
correct `release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n###
Identify risks\r\n\r\nThis PR contains a significant change to the
Dashboard layout engine,\r\nwhich means that it carries a decent amount
of risk for introducing new,\r\nuncaught bugs with dragging / resizing
panels and collision resolution.\r\nThat being said, `kbn-grid-layout`
has been built **iteratively** with\r\nplenty of testing along the way
to reduce this risk.\r\n\r\n## Release note\r\nImproves Dashboard layout
engine by switching to the internally\r\ndeveloped
`kbn-grid-layout`.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>\r\nCo-authored-by:
Marta Bondyra
<[email protected]>","sha":"686571547f82f097d589776733e165466ec518ad"}}]}]
BACKPORT-->
viduni94 pushed a commit to viduni94/kibana that referenced this pull request Jan 23, 2025
…lastic#204134)

Closes elastic#191306

## Summary

This PR updates the collision resolution algorithm to handle the edge
case described in the linked issue.

In the old algorithm, we checked each panel for collisions a **single
time** - however, this caused a bug where panels could get re-ordered
depending on the order that we pushed panels down + how much distance we
were pushing them down by. While this algorithm was extremely efficient
(approximately $`O \left({ { n^2 + n } \over 2 }\right)`$ before
compaction where `n` is the number of panels), it was not as simple as
"fixing" the bug because, no matter what, there was always an edge case
that could not be handled without adding some sort of backtracking -
instead, we had to come up with a new algorithm.

### Example of Edge Case

To better understand how the old and new algorithms compare (and how
they work), consider a scenario where the grid layout has 8 columns and
the panels are laid out as follows:


![image](https://github.com/user-attachments/assets/94fbb8f6-b6b8-4f9e-af91-0128717cdffc)

Now, what happens when panel 4 (yellow) is dragged to the left so that
it overlaps both panel 3 (red) and panel 4 (blue)?


<details>
<summary><b>Old Algorithm</b></summary>

With the old algorithm, we get the following behaviour:
  
![Dec-16-2024
17-04-58](https://github.com/user-attachments/assets/9a920046-cf53-4dff-bfbe-6ed0aed05f77)

This is because we are **only** pushing the collided panels down
**once**. So, to describe what happens in detail, we start by gathering
up the panels **in order** (top-bottom, left-right, where dragged panels
take priority when rows are equal) - this results in an order of
`[panel4, panel3, panel1, panel2]`.

We start checking for collisions in this order - starting with panel 4
(yellow), we determine that it is colliding with panel 3 (red) and panel
2 (blue). We start with panel 3 (since we are once again doing this "in
order"), and we push it down by 5 rows (since this is the overlap of the
panel 3 and panel 4). Then, to "resolve" the second collision with panel
4, we push down panel 2 by 3 rows. This leaves the layout in the
following state:

   

![image](https://github.com/user-attachments/assets/47aa981e-9e4f-4c30-8570-abf6ba4a20ba)

Now, since panel 4 (yellow) is "resolved", we move on to the next panel
in the `[panel4, panel3, panel1, panel2]` order - panel 3 (red). Panel 3
now collides with panel 2, so we push panel 2 down by two rows. This is
the only collision with panel 3, so we check panel 1 and panel 2,
neither of which report collisions at this point. So the layout ends in
the following state:


![image](https://github.com/user-attachments/assets/4c5e3aca-399c-4075-9897-a5d2d73e9284)

So, what's the problem here? Well, panel 1 (green) is now "out of sync"
with the old panel ordering because it got "left behind" in the
collision resolution. Instead, we should have moved panel 1 by the same
amount as panel 2.

</details>


<details>
<summary><b>New Algorithm</b></summary>

With the new algorithm, we get the expected behaviour:

![Dec-16-2024
17-30-28](https://github.com/user-attachments/assets/4cd7d49d-138b-4b59-ac78-0ea2cbc86c3c)

If we run through the algorithm, we identify that the **first**
collision in the layout (based on left to right, top to bottom ordering,
with priority being the panel being dragged) is panel 4 (yellow) - so,
we enter the `while` loop and recurse to resolve all collisions with
panel 4 via `resolvePanelCollisions`. The recursion within
`resolvePanelCollisions` works as follows - get **all** collisions with
panel 4 (which, at this point, returns panel 3 (red) and panel 2 (blue))
and push them down by a single row, then recurse on **those** panels to
resolve any of the resulting collisions.

It's difficult to describe recursion, but let me try my best :) During
the collision resolution on panel 4, we start by pushing panel 3 down
and then recursively resolving the collisions on panel 3. Since pushing
panel 3 causes collisions with panel 1 and 2 (in order), we push them
both down by one row - starting with panel 1. We recurse to resolve the
collisions on panel 1, which has no collisions - then we do the same for
panel 2, which also reports no collisions. The collisions with panel 3
are "resolved" (i.e. we can "back out" of the recursion on panels 1 and
2), so we go back to resolving panel 4, push panel **2** down, and then
resolve the collisions on panel 2 - which reports no collisions. In
summary:


![image](https://github.com/user-attachments/assets/ca82ca05-673e-4bf1-b22f-e2aa1c7b6ba4)


Therefore, after the first iteration of the `while` loop, this results
in the following layout, where panel 3 (red) has been pushed down by one
row, panel 1 (green) has been pushed down by one row, and panel 2 (blue)
has been pushed down by two rows:


![image](https://github.com/user-attachments/assets/ec7c08a5-d0e9-46f7-bf3c-2eede7cef6fd)

We then return to the `while` loop and check if the layout has any other
collisions - and panel 4 is still our "first" collision! So we repeat
the process above and push both panel 4 and panel 2 down and recurse to
resolve the resulting collisions on those panels. This continues as
follows:

| Steps | Resulting Layout |
|--------|--------|
| <img width="400"
src="https://github.com/user-attachments/assets/d2691a1c-5f0b-4333-bfd4-3cc57e1a3098"/>
| <img width="400"
src="https://github.com/user-attachments/assets/85325df3-7fbf-46fa-af98-10bbd883cf8d"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/e31a400f-d0d1-408d-baec-efa708ad0c52"/>
| <img width="400"
src="https://github.com/user-attachments/assets/c81762d4-443b-451c-b8fc-dabaf4e32ba1"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/4292e4a9-4157-4d77-9b69-b0f6a07338ac"/>
| <img width="400"
src="https://github.com/user-attachments/assets/bdd2dad5-6531-4d56-b8c2-dfb121fb6b5b"/>
|
| <img width="400"
src="https://github.com/user-attachments/assets/6d194a89-3ed6-46a6-9dcc-2b3fa0de9942"/>
| <img width="400"
src="https://github.com/user-attachments/assets/7e0b0fb5-bacb-49ad-ac86-02665a779b59"/>
|

And so, after our fifth iteration of the `while` loop, the layout is
**no longer** reporting collisions - so, our layout is now fully
resolved!

</details>

### Performance Comparison

This algorithm is more-or-less the same as the algorithm used in
`react-grid-layout` - however, instead of running on **every frame**,
our layout engine only resolves collisions **as they happen** (hence why
the while loop is necessary). This results in much better rendering
performance.

To test this, I opened the Logs sample dashboard in both the example app
(which is obviously using `kbn-grid-layout`) and Dashboard (which is
still using `react-grid-layout`) and performed various drag actions on
both while recording their performance. For example, consider the
following capture of a drag that doesn't cause **any** collisions:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/7fa1e9bb-e3dd-4f99-b011-0a46e53f0e88)
|
![image](https://github.com/user-attachments/assets/05e86a48-769d-4bae-ba48-d2fb3bdde90e)
|

You can see that, even with no collisions, the performance is slightly
better with `kbn-grid-layout` because we don't **ever** recurse on
collisions in this scenario. But it gets even better when actual
collisions happen - for example, consider the performance when the
dragged panel only causes a **single** collision:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------| 
|
![image](https://github.com/user-attachments/assets/5e39a616-aa96-43be-a316-cb8fc4258b88)
|
![image](https://github.com/user-attachments/assets/520ab5b6-7049-4028-9d8c-63972aff9e92)
|


Versus when the dragged panel causes multiple collisions at the top of
the dashboard that causes a chain reaction of panels needing to be
pushed down:

| `kbn-grid-layout` | `react-grid-layout` |
|--------|--------| 
|
![image](https://github.com/user-attachments/assets/4d6b15c6-181a-4ea0-a179-09702b50411a)
|
![image](https://github.com/user-attachments/assets/363ef360-3c4f-4327-8031-18aca3833bce)
|

In all of the above scenarios, performance is improved overall **and**
there are fewer red "spikes" / bottlenecks due to [forced
reflow](https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing?utm_source=devtools#avoid-forced-synchronous-layouts)
along the way. Most of the time saved is caused by significant
reductions to both `rendering` and `painting` time in `kbn-grid-layout`
when compared to `react-grid-layout`. While some of this improvement may
be explained by differences in the example grid plugin versus the
Dashboard plugin, this is enough proof that performance of
`kbn-grid-layout` is **at least as good as** `react-grid-layout` and so
swapping out the layout engine should not cause any performance
regressions.


### Other Notable Algorithm Attempts

I tried a few **drag/resize event specific** algorithms that **only
work** when an interaction event is happening - i.e. they rely on
information from the panel that is being interacted with in order to
function.

Note that, if we decided to go this route, we would still need some
generic collision resolution algorithm (such as the one proposed in this
PR) that we could use to resolve a layout when **no** interaction event
is happening. After all, we cannot assume that the grid layout that is
sent as a prop is valid (i.e. it **could** have overlapping panels **at
the start**, before any dragging and/or resizing has occurred) and we
need a way to verify + resolve it.

Unfortunately, both of the "interaction-event necessary" collision
resolution algorithms that I tried suffered from bugs so, while it might
be **possible** to make a more efficient algorithm for this, I
ultimately threw them away in favour of using the "generic" collision
resolution algorithm proposed in this PR.

That being said, I figured it was worth documenting the things that I
tried and why they failed.

<details>
<summary><b>Algorithm 1</b></summary>

I tried a modification of the **original** collision resolution
algorithm where, instead of **just** moving the colliding panels down by
the `rowOverlap` amount, we move **all** panels in or below the targeted
grid row down by the **height** of the panel being dragged. However, due
to the compaction algorithm ignoring **all** panels that have
collisions, you can end up in an infinite "push panels down" loop like
so:
 
![Dec-16-2024
15-31-22](https://github.com/user-attachments/assets/29364168-6bd5-4b62-839e-a63636af71f5)

This is because panel 3 is **always** colliding with panel 1 and 4, so
they never get pushed up to "close" the gaps. To try and get around
this, I modified the collision detection used for panel compaction so
that collisions only "stop" compaction if the collision occurs on a
panel **above** the given panel we are trying to compact. However, this
caused a **different** bug:

![Dec-16-2024
15-12-22](https://github.com/user-attachments/assets/34f23bfd-6a5d-4ff5-8421-10b7bab89551)

In the above GIF, consider what happens when panel 3 (red) targets row
3. As expected, we move all other panels (1, 2, and 4) down by a single
row. Then, when we reach the compaction step, we start by trying to push
panel 1 up (since it is now the top-left-most panel) - and, since we are
filtering out the collision with panel 3 (since it occurs "below" panel
1), it gets pushed all the way to the top. Then, we push panel 4 all the
way to the top (because it is also ignoring the collision with panel 3),
followed by pushing panel 2 up until it would collide with panel 3.
Unfortunately though, we didn't account for the full height of panel 4 -
so panel 3 is now stuck colliding with panel 4.

I modified this algorithm **further** to move the panels down by the
"row overlap" with the dragged panel rather than the dragged panel's
height - but this also suffers from a bug:

![Dec-16-2024
16-29-12](https://github.com/user-attachments/assets/950cb573-0caf-4c44-8f63-2fc465c2c497)
   
At which point, I abandoned this approach becase it felt like there were
too many edge cases that could not all be accounted for. It's possible
we could explore this further but, given the limitations of this
algorithm **only working** for resolving collisions given an interaction
event, I opted to move on.
</details>

<details>
<summary><b>Algorithm 2</b></summary>

I tried an algorithm where, similar to the algorithm proposed in this
PR, I recorded the panel IDs in each cell that a given panel occupied -
however, instead of moving things down row by row and handling the
resulting collisions, we moved **every** panel that was **below** the
targetted row down by the **height** of the panel being dragged. In
theory, the compaction algorithm would then take control of closing all
the resulting gaps.
  
Unfortunately, this didn't quite work in practice. Consider the
following:
  
![Dec-16-2024
10-17-21](https://github.com/user-attachments/assets/8157dabd-bb52-4089-9493-74cbdcf49e5e)
 
  
According to the algorithm defined above, panel 2 (blue) and panel 4
(yellow) both need to move down when panel 3 (red) is dragged to target
row 4 (remember that row **zero** is the first row), since they both
occupy this row - so we push both of them down by 1 (the height of panel
3). However, once we start compacting the layout in order to finish our
collision resolution, we hit a snag with panel 4 (yellow) - when we
start trying to compact panel 4, `getAllCollisionsWithPanel` returns
that **panel 3** is colliding with panel 4 and so we **break out** of
the compaction - hence why panel 4 continues to float. This then has a
snowball effect on all other panels as they report collisions and
therefore do not get pushed up.
</details>
 

<details>
<summary><b>Algorithm 3</b></summary>

This was the algorithm we **almost** went with, because it works! But it
suffered from two things - one, the code was complicated and difficult
to maintain and two, the collision resolution behaviour felt less
natural because it **always** favoured the dragged panel. This made it
too sensitive to pushing items down; so even if a panel was targeting
the bottom row of a different panel, it would push that panel down to
make room for the dragged panel.

In the simplest terms, this algorithm worked as follows: after creating
a 3D array representing the current grid (a 2D array of panel ID
arrays), for each row, while there are collisions, push panels down in
**reverse order** (bottom right to top left); continue until all rows
have no collisions. This algorithm hd an efficiency of approximately
$`O\left(r * c \right)`$ to set up the 3D array and $`O \left( c * h * n
\right)`$ to resolve the conflicts, where `r` is the number of rows, `c`
is the number of columns (minus 1), `h` is the height of the dragged
panel, and `n` is the number of panels. This results in an overall speed
of about $`O \left( (r * c) + (c * h * n) \right)`$

To better understand how this algorithm works, consider how it handles
the edge case described above:


<details>
<summary>Algorithm 3 - Edge Case Example</summary>

Rather than checking every **panel** for collisions a single time, we
check and resolve each **row** for collisions and resolve them. This
gives the following behaviour:


![Dec-16-2024
17-30-28](https://github.com/user-attachments/assets/60cf3347-707c-4bc2-99e5-0e042c4cbed3)

We start by creating a 3D array representing the grid - at the **first**
point of collision, it looks like:

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **1** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **2** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **3** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |
| **4** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |

Then, we start checking and resolving collisions, row by row. Starting
with row 0, we see that column 3 has a collision between panel 3 and
panel 4 - we **ignore** the panel being dragged (panel 4), so we push
down panel 3 by one row. Row 0 has no more collisions, so we move on to
row 1 and, following the same logic as above, we push down panel 3
again. This leaves the grid in the following state:


![image](https://github.com/user-attachments/assets/47ee1f8c-5107-41eb-8309-527e6fd773be)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **3** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **4** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2, panel4]` |
`[panel4]` | `[panel4]` | `[panel4]` | `[]` |

Now, on row 2, we have collisions in columns zero through three, so we
push the panels in **reverse order** (i.e from the bottom right to the
top left) - this results in pushing panel 2 down, then panel 1, and
finally panel 3. We have to add a row in order to accomplish this
because otherwise we "lose" the bottom of panel 2, so our layout now
looks like this and row 2 has no more collisions:
   

![image](https://github.com/user-attachments/assets/b5697592-cb0e-4fac-90c9-6f3ade5e9db9)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **4** | `[panel1, panel3]` | `[panel1, panel3]` | `[panel2, panel3]` |
`[panel3, panel2, panel4]` | `[panel4]` | `[panel4]` | `[panel4]` | `[]`
|
| **5** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

Continuing this behaviour, we push panel 2, panel 1, and panel 3 down in
row 3; then again in row 4. Once we reach and resolve row 5 (i.e. get
row 5 to the point where there are no collisions with panel 3), our
layout looks like so:


![image](https://github.com/user-attachments/assets/e59de766-a547-499a-a181-7841179032c0)
   
|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **4** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **5** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **6** | `[panel3, panel1]` | `[panel3, panel1]` | `[panel2, panel3]` |
`[panel2, panel3]` | `[]` | `[]` | `[]` | `[]` |
| **7** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **8** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

At this point, all collisions on panel 4 have been resolved. So, in row
6, we see that panels 2 and 1 are now colliding with panel 3 - so, we
push both of them down. At that point, row 6 does not have collisions so
we **don't** push panel 3 down any further - and our layout is resolved!
    

![image](https://github.com/user-attachments/assets/71869176-473f-4a48-8d0f-863b855628c4)

|   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

|--------|--------|--------|--------|--------|--------|--------|--------|--------|
| **0** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **1** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **2** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **3** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **4** | `[]` | `[]` | `[]` | `[panel4]` | `[panel4]` | `[panel4]` |
`[panel4]` | `[]` |
| **5** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **6** | `[panel3]` | `[panel3]` | `[panel3]` | `[panel3]` | `[]` |
`[]` | `[]` | `[]` |
| **7** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **8** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |
| **9** | `[panel1]` | `[panel1]` | `[panel2]` | `[panel2]` | `[]` |
`[]` | `[]` | `[]` |

</details>

</details>


### 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)

### Identify risks

There are no risks to this PR, since all work is contained in the
`examples` plugin.

---------

Co-authored-by: kibanamachine <[email protected]>
viduni94 pushed a commit to viduni94/kibana that referenced this pull request Jan 23, 2025
…d-layout` (elastic#205341)

Closes elastic#190446

## Summary

This PR swaps out `react-grid-layout` for the new internal
`kbn-grid-layout` in the Dashboard plugin. This is the first major step
in making collapsible sections possible in Dashboard.

- **`react-grid-layout` (before)**:


https://github.com/user-attachments/assets/ca6ec059-7f4a-43fb-890e-7b72b781e50b

- **`kbn-grid-layout` (after)**:


https://github.com/user-attachments/assets/3d3de1f3-1afc-4e6b-93d6-9cc31a46e2cf

### Notable Improvements

- Better handling of resizing panels near the bottom of the screen
   
  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Jan-09-2025
09-59-00](https://github.com/user-attachments/assets/75854b76-3ad7-4f06-9745-b03bde15f87a)
| ![Jan-09-2025
09-26-24](https://github.com/user-attachments/assets/f0fbc0bf-9208-4866-b7eb-988c7abc3e50)
|


- Auto-scroll when dragging / resizing panels near the top and bottom of
the screen, making it much easier to move panels around by larger
distances

  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Jan-09-2025
10-01-30](https://github.com/user-attachments/assets/e3457e5e-3647-4024-b6e6-c594d6d3e1d7)
| ![Jan-09-2025
09-25-35](https://github.com/user-attachments/assets/3252bdec-2bbc-4793-b089-346866d4589b)
|

- More reliable panel positioning due to the use of CSS grid rather than
absolute positioning via pixels

  | `react-grid-layout` | `kbn-grid-layout` |
  |--------|--------|
| ![Screenshot 2025-01-09 at 9 32
52 AM](https://github.com/user-attachments/assets/06bd31a4-0a9f-4561-84c3-4cd96ba297b0)
| ![Screenshot 2025-01-09 at 9 35
14 AM](https://github.com/user-attachments/assets/573dab98-3fb9-4ef6-9f37-c4cf4d03ce52)
|

- Better performance when dragging and resizing (see
elastic#204134 for a more thorough
explanation) and a smaller bundle size than `react-grid-layout`

### 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)

### Identify risks

This PR contains a significant change to the Dashboard layout engine,
which means that it carries a decent amount of risk for introducing new,
uncaught bugs with dragging / resizing panels and collision resolution.
That being said, `kbn-grid-layout` has been built **iteratively** with
plenty of testing along the way to reduce this risk.

## Release note
Improves Dashboard layout engine by switching to the internally
developed `kbn-grid-layout`.

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Marta Bondyra <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:prev-minor Backport to (8.x) the previous minor version (i.e. one version back from main) Feature:Dashboard Dashboard related features impact:high Addressing this issue will have a high level of impact on the quality/strength of our product. loe:medium Medium Level of Effort release_note:skip Skip the PR/issue when compiling release notes Team:Presentation Presentation Team for Dashboard, Input Controls, and Canvas v9.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Dashboard][Collapsable Panels] Fix panel order edge case
4 participants