-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
Add setHeaderActionMenu API to AppMountParameters #75422
Add setHeaderActionMenu API to AppMountParameters #75422
Conversation
Pinging @elastic/kibana-core-ui (Team:Core UI) |
Pinging @elastic/kibana-platform (Team:Platform) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security changes LGTM -- code review only
ping @joshdover @myasonik @ryankeairns @lizozom PTAL regarding the actual API. As there seems to be some misunderstanding, I prefer to clarify: We won't be able to directly / automatically use the existing component (https://github.com/elastic/kibana/blob/master/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx) without any changes inside the existing applications code. What we are doing is exposing a way for applications to render the menu / topbar component inside the chrome header. But code adaptation will still be needed in every app to mount the component using the new Now, regarding the 'raw' We could, instead of the API proposed in this PR, expose a 'higher' level API that duplicates what is currently done in That would mean:
kibana/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx Lines 71 to 85 in 38b9a8d
and change export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => {
const { renderApp } = await import('./application');
const { renderActionMenu } = await import('./action_menu');
setHeaderActionMenuData([{id: 'foo', run: () => doSomething()...}]) // TopNavMenuData[]
return renderApp({ element, history });
} However, that would still require very similar changes for applications currently using the existing export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => {
const { renderApp } = await import('./application');
const { renderActionMenu } = await import('./action_menu');
setHeaderActionMenu((element) => {
return navigation.mountMenu(element, myMenuConfig); // Element, TopNavMenuData[]
})
return renderApp({ element, history });
} But once again I have to insist on the fact that changes inside the applications currently using Also note that if none of these changes would (probably) be too heavy, some might be non-trivial, and I think letting the core-ui and/or platform team performing these changes instead of the legitimate owners may be a risk. For instance, the way this is done in the dashboard app is quite nested, inside a 'legacy' angular controller that atm does not have access to the app's kibana/src/plugins/dashboard/public/application/dashboard_app_controller.tsx Lines 681 to 684 in 9bef317
Did not look at all usages, but others a probably similar. |
In order to smooth over the usage of this with React, I think we could offer a special Example: const MountPointPortal: React.FC<{
setMountPoint: (mountPoint: MountPoint<HTMLElement> | undefined) => void;
}> = ({ children, setMountPoint }) => {
// state used to force re-renders when the element changes
const [shouldRender, setShouldRender] = useState(false);
const el = useRef<HTMLElement>();
useEffect(() => {
setMountPoint((element) => {
el.current = element;
setShouldRender(true);
return () => {
setShouldRender(false);
el.current = undefined;
};
});
return () => {
setShouldRender(false);
el.current = undefined;
};
}, [setMountPoint]);
if (shouldRender && el.current) {
return ReactDOM.createPortal(children, el.current);
} else {
return null;
}
}; Applications could then use this pretty transparently: const MyApp = ({ params }) => {
const [someState, setSomeState] = useState(1);
return (
<Router history={params.history}>
<MountPointPortal setMountPoint={params.setHeaderActionMenu}>
<TopNavMenu someProp={someState} /> {/** state updates should still work here */}
</MountPointPortal>
{/** Rest of UI tree here */}
</Router>
);
} I wasn't able to really test this on this PR since this is not yet integrated with the new Header PR. |
Re-capping for the non-eng audience, it sounds like there is an unavoidable bit of work that each app team (which currently uses the I'm asking 1) for my own understanding and 2) in order to start planning how we roll out this message/request. Thanks for working on this important change! |
@ryankeairns both assertions are correct. @joshdover I don't know why I didn't think about mounting the portal inside the mountpoint's provided element. I think this should work. ping @elastic/kibana-app-arch to continue on that direction, I gonna need to make some changes and additions to the
I could use the component directly by passing
setHeaderActionMenu((element) => navigation.mountMenu(element, myMenuConfig)) Do you have any remark or objection regarding these changes? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dev Tools change LGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ML code edit LGTM
@pgayvallet If you plan to extract the menu rendering of TopNavMenu to it's own component, why can't Not sure I understood the last extraction you described. Also, does this API allow an app to change the menu during the runtime of the application (like switching a dashboard from and to edit mode). Just want to make sure this case was taken into account. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Introduced the MountPointPortal
component and adapted TopNavMenu
to accept an optional setMenuMountPoint
property.
@joshdover @lizozom PTAL
.kbnTopNavMenu { | ||
padding: $euiSizeS 0; | ||
|
||
.kbnTopNavItemEmphasized { | ||
padding: 0 $euiSizeS; | ||
} | ||
.kbnTopNavItemEmphasized { | ||
padding: 0 $euiSizeS; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The menu can now be rendered outside the TopNavMenu
component due to the portal. I had to move the .kbnTopNavMenu
styles outside of .kbnTopNavMenu__wrapper
.
Note: there will probably be some style tweaking depending on the display, but this will have to be handled during the next step in #72331
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The next EUI update will likely address some of this. I can assist Michail with any additional touch ups. Thanks!
<span className="kbnTopNavMenu__wrapper"> | ||
{renderMenu(className)} | ||
{setMenuMountPoint ? ( | ||
<MountPointPortal setMountPoint={setMenuMountPoint}> | ||
{renderMenu(className)} | ||
</MountPointPortal> | ||
) : ( | ||
renderMenu(className) | ||
)} | ||
{renderSearchBar()} | ||
</span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if that would be preferable, when we are rendering via portal (setMenuMountPoint
is defined), to duplicate the wrapper and do something like
if (setMenuMountPoint) {
return (
<>
<MountPointPortal setMountPoint={setMenuMountPoint}>
<span className="kbnTopNavMenu__wrapper">{renderMenu(className)}</span>
</MountPointPortal>
<span className="kbnTopNavMenu__wrapper">{renderSearchBar()}</span>
</>
);
} else {
return (
<span className="kbnTopNavMenu__wrapper">
{renderMenu(className)}
{renderSearchBar()}
</span>
);
}
wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer the duplicated code for readability 👍
May want to extract the className of the span into a variable that can be reused to prevent inconsistency bugs from changes to the classname.
// menu is rendered outside of the component | ||
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); | ||
expect(portalTarget.getElementsByTagName('BUTTON').length).toBe(menuItems.length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As the portal is rendered outside the enzyme root element, we are forced to fallback to plain html API for this test.
private refreshCurrentActionMenu = () => { | ||
const appId = this.currentAppId$.getValue(); | ||
const currentActionMenu = appId ? this.appInternalStates.get(appId)?.actionMenu : undefined; | ||
this.currentActionMenu$.next(currentActionMenu); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe not worth doing until after we complete #74911, but it feels like there's a lot of state & update juggling going on in this service now. Seems like it would be better to have a single source of truth that everything else responds to. wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, and I also think we should wait until legacy mode has been removed to cleanup that thing
*/ | ||
export const MountPointPortal: React.FC<MountPointPortalProps> = ({ children, setMountPoint }) => { | ||
// state used to force re-renders when the element changes | ||
const [shouldRender, setShouldRender] = useState(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
heh, I was hoping you had a more elegant idea than me, but glad it works!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK a state-based trick can't be avoid needed here unfortunately, as we can't 'observe' the changes on the reference.
|
||
await refresh(); | ||
|
||
expect(portalTarget.textContent).toBe(''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: innerHTML
may be a better property to test against since textContent
can be an empty string even if there are other DOM node children
}, [setMountPoint]); | ||
|
||
if (shouldRender && el.current) { | ||
return ReactDOM.createPortal(children, el.current); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we have a default error boundary here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasn't sure. Added it.
<span className="kbnTopNavMenu__wrapper"> | ||
{renderMenu(className)} | ||
{setMenuMountPoint ? ( | ||
<MountPointPortal setMountPoint={setMenuMountPoint}> | ||
{renderMenu(className)} | ||
</MountPointPortal> | ||
) : ( | ||
renderMenu(className) | ||
)} | ||
{renderSearchBar()} | ||
</span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer the duplicated code for readability 👍
May want to extract the className of the span into a variable that can be reused to prevent inconsistency bugs from changes to the classname.
@lizozom can we get your review and approval on this one? |
💚 Build SucceededBuild metrics@kbn/optimizer bundle module count
async chunks size
page load bundle size
History
To update your PR or re-run it, just comment with: |
* add `setHeaderActionMenu` to AppMountParameters * allow to remove the current menu by calling handler with undefined * update generated doc * updating snapshots * fix legacy tests * call renderApp with params * rename toMountPoint component file for consistency * add the MountPointPortal utility component * adapt TopNavMenu to add optional `setMenuMountPoint` prop * add kibanaReact as required bundle. * use innerHTML instead of textContent for portal tests * add error boundaries to portal component * improve renderLayout readability * duplicate wrapper in portal mode to avoid altering styles Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
YESSSS thanks Pierre! |
* add `setHeaderActionMenu` to AppMountParameters * allow to remove the current menu by calling handler with undefined * update generated doc * updating snapshots * fix legacy tests * call renderApp with params * rename toMountPoint component file for consistency * add the MountPointPortal utility component * adapt TopNavMenu to add optional `setMenuMountPoint` prop * add kibanaReact as required bundle. * use innerHTML instead of textContent for portal tests * add error boundaries to portal component * improve renderLayout readability * duplicate wrapper in portal mode to avoid altering styles Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
* master: (223 commits) skip flaky suite (elastic#75724) [Reporting] Add functional test for Reports in non-default spaces (elastic#76053) [Enterprise Search] Fix various icons in dark mode (elastic#76430) skip flaky suite (elastic#76245) Add `auto` interval to histogram AggConfig (elastic#76001) [Resolver] generator uses setup_node_env (elastic#76422) [Ingest Manager] Support both zip & tar archives from Registry (elastic#76197) [Ingest Manager] Improve agent vs kibana version checks (elastic#76238) Manually building `KueryNode` for Fleet's routes (elastic#75693) remove dupe tinymath section (elastic#76093) Create APM issue template (elastic#76362) Delete unused file. (elastic#76386) [SECURITY_SOLUTION][ENDPOINT] Trusted Apps Create API (elastic#76178) [Detections Engine] Add Alert actions to the Timeline (elastic#73228) [Dashboard First] Library Notification (elastic#76122) [Maps] Add mvt support for ES doc sources (elastic#75698) Add setHeaderActionMenu API to AppMountParameters (elastic#75422) [ML] Remove "Are you sure" from data frame analytics jobs (elastic#76214) [yarn] remove typings-tester, use @ts-expect-error (elastic#76341) [Reporting/CSV] Do not fail the job if scroll ID can not be cleared (elastic#76014) ...
Summary
Part of #68524
Related to #72331
AppMountParameters.setHeaderActionMenu
to allow application to register header application menucurrentActionMenu$
from application service internal start contractNote: actual consumption of
currentActionMenu
will be done in #72331, as the new header in not present onmaster
.Example
Checklist