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

[EuiDataGrid] Add new cellContext prop/API #7374

Merged
merged 23 commits into from
Mar 7, 2024

Conversation

kqualters-elastic
Copy link
Contributor

@kqualters-elastic kqualters-elastic commented Nov 18, 2023

Summary

Currently, it's very difficult in Kibana for a plugin that provides a common usage of EuiDataGrid (ex: triggers_actions_ui) to allow plugins that consume the dependent plugin (ex: security solution) a way to define the renderCellValue prop of EuiDataGrid with all of their custom props and solution specific logic, while allowing the providing plugin a way to also inject props/change the jsx returned. This was at first solved with the first usages of EuiDataGrid across kibana with a whole lot of prop drilling (ex: https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx#L23 and it's usages) and then in the latest iteration, by having the consuming plugin 'register' functions when the plugin starts to use as hooks by the service providing plugin at runtime. This approach does not require drilling props, but is imo worse because it's very difficult (I have not been able to figure it out, and have tried a whole bunch of different things) to pass application specific props from both the consuming and service providing plugins, without either violating the rules of hooks and causing react to throw runtime errors, or by causing every cell in every table to re-render whenever any parent component does, because the renderCellValue function is referentially new each time. https://github.com/elastic/kibana/blob/main/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx#L371-#L429 This is maybe not a big deal in the parts of kibana where there's only ever a single table, or very little complexity is needed in cells, but in security solution, where users will often manipulate the timeline table in a modal with the alerts table open underneath, or where we have cells with lots of permissions, row cell actions that fire network requests, etc etc, this is absolutely brutal for overall performance and a massive problem.

This branch proposes a new solution to this problem by introducing a new optional prop, renderCellContext, with a type of:

type CellContext = Omit<
  Record<string, any>,
  keyof EuiDataGridCellValueElementProps
>;
type CellPropsWithContext = CellContext & EuiDataGridCellValueElementProps;

that allows a service providing plugin to pass in extra props to EuiDataGrid, and then the renderCellValue prop can be a normal, memoized component, that has known type safe props. The service providing plugin can take this another step further, and allow the consuming plugin to pass their own definition of these props, combine the context values as needed, and go from there.

QA

General checklist

  • Browser QA - N/A
  • Docs site QA
  • Code quality checklist
  • Release checklist
    • A changelog entry exists and is marked appropriately.
      - [ ] If applicable, added the breaking change issue label (and filled out the breaking change checklist)
  • Designer checklist - N/A

@kqualters-elastic kqualters-elastic changed the title [EuiDataGrid] Row cell context proposal [EuiDataGrid] renderCellContext proposal Nov 18, 2023
@kqualters-elastic
Copy link
Contributor Author

Would also solve #5811

@@ -115,7 +130,7 @@ const EuiDataGridCellContent: FunctionComponent<
rowIndex={rowIndex}
colIndex={colIndex}
schema={column?.schema || rest.columnType}
{...rest}
{...mergedProps}
Copy link
Contributor

Choose a reason for hiding this comment

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

Tell me if I am wrong but I think the spreading is not going to help us here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not sure what you mean. If any of the props change, this will all re-render, but consumers can short circuit everything via memo if everything in ...renderCellContext() is referentially equal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

or another benefit, which might be pretty impactful for us, is if renderCellContext has only 1 prop that changes, and is used by only 1 of our RenderCellValue components, the others do not render. I think that will be especially impactful for our normal cells, not updating when inputs query params in the url change (and so refresh() needs to change), this would result in only 1/12th the cells re-rendering

<EuiCheckbox
id={`select-row-${rowIndex}`}
aria-label="Select row"
onChange={() => {}}

Choose a reason for hiding this comment

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

Does the anonymous function get re-created on re-renders? Technically this array should be stable since it's outside the render, but not sure if it's worth passing const noOp = () => {} to this and the headerCellRender below?

Choose a reason for hiding this comment

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

Ignore, I see this is docs

renderCellValue={({ rowIndex, columnId }) =>
raw_data[rowIndex][columnId]
}
renderCellValue={renderCellValue}

Choose a reason for hiding this comment

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

👍🏾

@kqualters-elastic
Copy link
Contributor Author

The test failure is only happening on react 16, seems sort of odd. Snapshot tests are imo of limited use, so could delete that one, or if there's something specific to test for I can update, just let me know.

@cee-chen
Copy link
Member

cee-chen commented Nov 21, 2023

After having had a chance to take a skim at this PR the larger size of it is giving me a bit more pause than I originally anticipated. @kqualters-elastic I may end up breaking out some of the more bite-sized changes into separate PRs after all (e.g. skeleton text) just for ease of review.

The other question I have for y'all is how soon you want this in Kibana / if you need it in before 8.12 FF - we unfortunately have another large set of EuiDataGrid changes in flight, that need to be in for 8.12, and will 100% cause merge conflicts with this one (cell actions redesign, will have DOM changes).

@kqualters-elastic
Copy link
Contributor Author

@cee-chen re: the skeleton text issue, that's imo more of a separate bug, can merge whenever. Although I do think I should mention that this problem is pretty widespread in eui. https://github.com/elastic/eui/blob/main/src/components/basic_table/default_item_action.tsx#L73 is another example, there are 10s-100s of them, and any place where components like that (not memoized, reinitializing variables in the middle of a render in a function component) where parent components are updating very frequently, could see pretty bad performance, as all of that jsx in the component is created new and destroyed each render. Does not matter to me when the datagrid specific changes get in, but the performance as of today is pretty bad, and so sooner would be better than later.

@cee-chen
Copy link
Member

cee-chen commented Nov 22, 2023

Not having a specific deadline for this work is a huge relief, thanks Kevin! (we have some unfortunate holiday timing and team-wide travel coming up). I can definitely prioritize this PR after the 8.12 FF work gets done.

Just curious, do you know of any kind of automated linter or scanning/perf tool we can use to catch those problematic instances? If so, I'd propose installing that tool on EUI to try and update them all at once rather than just for datagrid etc.

@kqualters-elastic
Copy link
Contributor Author

Just curious, do you know of any kind of automated linter or scanning/perf tool we can use to catch those problematic instances?

@cee-chen I think this eslint rule might detect a good amount of them https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unstable-nested-components.md

@kqualters-elastic
Copy link
Contributor Author

https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-constructed-context-values.md might not be a bad idea either

@kqualters-elastic
Copy link
Contributor Author

What that lint rule won't catch is stuff like this: https://github.com/elastic/eui/blob/main/src/components/text/text.tsx#L49 and this https://github.com/elastic/eui/blob/main/src/components/text/text_align.tsx#L39 which is creating a new array on each render of css styles to inject into the app. The end result is something like this:
image
and is really just decimating the performance of kibana at the moment

@cee-chen
Copy link
Member

oof, I remember being mildly worried about Emotion's performance in that regard but thought I was overthinking it at the time 😬

We can likely write our own custom lint rule for Emotion styles (or make it a general best practice to memoize the arrays). If you don't mind testing - can you quickly confirm that wrapping a useMemo around the cssStyles array does improve/affect the results in the screenshot you posted?

@kqualters-elastic kqualters-elastic marked this pull request as ready for review March 5, 2024 00:01
@kqualters-elastic kqualters-elastic requested a review from a team as a code owner March 5, 2024 00:01
kqualters-elastic and others added 4 commits March 5, 2024 01:40
…from fn type to object/record

+ simplify `mergedProps` to spread operators

+ only pass context to `<CellElement />`, not to `<PopoverElement />`
@cee-chen cee-chen changed the title [EuiDataGrid] renderCellContext proposal [EuiDataGrid] Add new cellContext prop/API Mar 7, 2024
@cee-chen cee-chen self-assigned this Mar 7, 2024
Copy link
Member

@cee-chen cee-chen left a comment

Choose a reason for hiding this comment

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

Thanks for your patience with us while talking through this work Kevin!

As a note for other async readers, the performance changes in this PR have been moved to #7556, and this PR now only concerns itself with adding an API that makes it easy for consumers to pass data to static renderCellValue function components.

@cee-chen cee-chen enabled auto-merge (squash) March 7, 2024 19:16
@elasticmachine
Copy link
Collaborator

elasticmachine commented Mar 7, 2024

💚 Build Succeeded

History

cc @cee-chen

@kibanamachine
Copy link

Preview staging links for this PR:

@cee-chen cee-chen merged commit 0396b26 into elastic:main Mar 7, 2024
7 checks passed
cee-chen added a commit to elastic/kibana that referenced this pull request Mar 12, 2024
`v93.2.0`⏩`v93.3.0`

---

## [`v93.3.0`](https://github.com/elastic/eui/releases/v93.3.0)

- Added new `EuiDataGrid` new prop: `cellContext`, an optional object of
additional props passed to the cell render function.
([#7374](elastic/eui#7374))
- `EuiBreadcrumbs`'s `popoverContent` API now accepts a render function
that will be passed a `closePopover` callback, allowing consumers to
close the breadcrumb popover from their popover content
([#7555](elastic/eui#7555))

**Bug fixes**

- Fixed missing animation on native `EuiProgress` bar update
([#7538](elastic/eui#7538))
- Fixed an `EuiDataGrid` bug with `gridStyle.rowClasses`, where custom
consumer classes that began with `euiDataGridRow` would not be correctly
removed/reapplied ([#7549](elastic/eui#7549))
- Fixed a visual `EuiDataGrid` bug where `EuiCheckbox`es within control
columns were not vertically centered within single height rows
([#7549](elastic/eui#7549))
kqualters-elastic added a commit to elastic/kibana that referenced this pull request Apr 16, 2024
…180016)

## Summary

This pr makes use of a new prop (and some generic memoization fixes) in
2 eui prs merged recently (elastic/eui#7556 and
elastic/eui#7374) to improve the performance of
the alerts table. Mainly, the cellContext prop is now available to
consumers of the triggersActionsUi alerts table to pass in custom props
in a way that allows the renderCellValue/rowCellRender functions of the
various EuiDataGrid prop apis to remain referentially stable across
re-renders. There are also some various changes to various hooks and
props found throughout plugins that use the table to improve
performance. There should be no change in functionality, just a moderate
to substantial reduction in time the app spends rendering the alerts
table in various scenarios. Below will be some react dev tools
performance profiles, main compared to this branch, with the exact same
set of generated data.

Main, switching from 10-100 rows:

![main_alerts_table](https://github.com/elastic/kibana/assets/56408403/6b87093f-5b1b-4d22-8e23-ccc3406317f4)

This branch 10-100 rows:

![context_alerts_table](https://github.com/elastic/kibana/assets/56408403/75bf5d53-045d-42ae-979a-e52d6c0f8020)

Pretty modest gain here, 1 render is slower than any others on main, but
overall less time spent rendering by about a second.

Main, opening the cell popover on 2 cells

![main_open_cell_popover](https://github.com/elastic/kibana/assets/56408403/60c5d132-b526-4859-a29c-5d3157142d50)

This branch, opening cellpopover

![context_open_cell_popover](https://github.com/elastic/kibana/assets/56408403/2c60b250-6a9f-44b4-aec8-f9dcb8c87531)

Again nothing crazy here, modest improvement.

Main opening timeline and hitting refresh


![main_open_timeline](https://github.com/elastic/kibana/assets/56408403/7525200d-cf9b-4f43-9f24-43f314740db1)

This branch, opening timeline and hitting refresh


![context_open_timeline](https://github.com/elastic/kibana/assets/56408403/efd3cf95-a81a-4933-b310-fa61de24258f)

This is the case that brought this about in the first place, as security
solution shares a lot of code between tables, the alerts table
recreating all cells on every render was destroying performance of the
timeline rendering in a flyout/modal while users were on alerts page or
the rule detail page, which is probably the most common use case. 93ms
in this branch vs 2500+ ms in main. This type of performance hit happens
when almost any action is taken in timeline.

Main, selecting 3 alerts 1 at a time

![main_actions](https://github.com/elastic/kibana/assets/56408403/87487149-cf6d-4bcc-9192-b64411abe321)

This branch selecting 3 alerts 1 at a time


![context_actions](https://github.com/elastic/kibana/assets/56408403/8407953a-5af0-4cfd-9f2c-08c710c81fb3)

Pretty substantial improvement here as well, ~2000ms vs 67ms.

Apologies if some of the gifs are cut off, please try the branch vs main
on your own! This was done on a local kibana in dev mode, so things like
emotion are eating up more cpu than they would for a user, but there
still should be improvement, and I can't think of any circumstance where
things will be worse. Also this branch was a little longer lived than I
would have liked, so if you are reviewing and changed something around
these files recently please double check I didn't miss anything.



### 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants