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

Add setHeaderActionMenu API to AppMountParameters #75422

Merged
merged 23 commits into from
Sep 1, 2020

Conversation

pgayvallet
Copy link
Contributor

@pgayvallet pgayvallet commented Aug 19, 2020

Summary

Part of #68524
Related to #72331

  • Add AppMountParameters.setHeaderActionMenu to allow application to register header application menu
  • Expose currentActionMenu$ from application service internal start contract

Note: actual consumption of currentActionMenu will be done in #72331, as the new header in not present on master.

Example

export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => {
   const { renderApp } = await import('./application');
   const { renderActionMenu } = await import('./action_menu');
   setHeaderActionMenu((element) => {
     return renderActionMenu(element);
   })
   return renderApp({ element, history });
}

Checklist

@pgayvallet pgayvallet added v7.10.0 release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc labels Aug 19, 2020
@pgayvallet pgayvallet added the REASSIGN from Team:Core UI Deprecated label for old Core UI team label Aug 19, 2020
@pgayvallet pgayvallet marked this pull request as ready for review August 19, 2020 15:09
@pgayvallet pgayvallet requested review from a team as code owners August 19, 2020 15:09
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-core-ui (Team:Core UI)

@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-platform (Team:Platform)

Copy link
Contributor

@jportner jportner left a 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

@pgayvallet
Copy link
Contributor Author

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 setHeaderActionMenu API instead of manually rending inside their 'owned' dom as they were previously doing.

Now, regarding the 'raw' mountpoint vs duplicating the TopBarMenu / TopNavMenuData inside core:

We could, instead of the API proposed in this PR, expose a 'higher' level API that duplicates what is currently done in TopBarMenu.

That would mean:

  • duplicating the TopNavMenu code inside core

export interface TopNavMenuData {
id?: string;
label: string;
run: TopNavMenuAction;
description?: string;
testId?: string;
className?: string;
disableButton?: boolean | (() => boolean);
tooltip?: string | (() => string | undefined);
emphasize?: boolean;
iconType?: string;
iconSide?: EuiButtonProps['iconSide'];
}

function renderMenu(className: string): ReactElement | null {
if (!config || config.length === 0) return null;
return (
<EuiFlexGroup
data-test-subj="top-nav"
justifyContent="flexStart"
alignItems="center"
gutterSize="none"
className={className}
responsive={false}
>
{renderItems()}
</EuiFlexGroup>
);
}

and change setHeaderActionMenu to take the menu configuration instead of a MountPoint as parameter

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 TopNavMenu, as they would still need to access and call the new API from the application's mount parameters, so I feel like the mount point is more permissive. We could eventually add an helper method to the navigation plugin:

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 TopNavMenu will be required and can't be avoided.

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 AppMountParameters, meaning that this will need to be propagated down to it.

const dashboardNavBar = document.getElementById('dashboardChrome');
const updateNavBar = () => {
ReactDOM.render(<navigation.ui.TopNavMenu {...getNavBarProps()} />, dashboardNavBar);
};

Did not look at all usages, but others a probably similar.

@joshdover
Copy link
Contributor

In order to smooth over the usage of this with React, I think we could offer a special MountPointPortal component that allows you to leverage a mount point within a React application without losing the ability for state updates to propagate into UI tree rendered inside the mount point. This could complement or even potentially replace the toMountPoint utility we have today.

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.

@ryankeairns
Copy link
Contributor

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 TopNavMenu component) will have to complete. Also, if I'm understanding, Josh's comment provides a way to lessen or at least simplify the implementation for each affected team. Are those statements generally accurate?

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!

@pgayvallet
Copy link
Contributor Author

pgayvallet commented Aug 19, 2020

@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 kibana_react and the navigation plugins:

  • Add the MountPointPortal utility FC to kibana_react

  • extract the menu rendering of TopNavMenu to it's own component (renderMenu)

I could use the component directly by passing showSearchBar=false, but I think that as in the header, the component will never have to render the search bar, it is a little cleaner to extract and reuse a new 'menu-only' component, wdyt?

  • add the utility mountMenu API, to achieve the wanted API format:
setHeaderActionMenu((element) => navigation.mountMenu(element, myMenuConfig))

Do you have any remark or objection regarding these changes?

Copy link
Contributor

@cjcenizal cjcenizal left a 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.

Copy link
Contributor

@peteharverson peteharverson left a 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

@lizozom
Copy link
Contributor

lizozom commented Aug 20, 2020

@pgayvallet If you plan to extract the menu rendering of TopNavMenu to it's own component, why can't TopNavMenu use the new API internally, so to avoid apps having to do extra work?

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.

@pgayvallet pgayvallet requested a review from a team as a code owner August 24, 2020 08:31
@pgayvallet pgayvallet requested a review from a team as a code owner August 24, 2020 09:08
Copy link
Contributor Author

@pgayvallet pgayvallet left a 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

Comment on lines 5 to 11
.kbnTopNavMenu {
padding: $euiSizeS 0;

.kbnTopNavItemEmphasized {
padding: 0 $euiSizeS;
}
.kbnTopNavItemEmphasized {
padding: 0 $euiSizeS;
}
}
Copy link
Contributor Author

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

Copy link
Contributor

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!

Comment on lines 118 to 127
<span className="kbnTopNavMenu__wrapper">
{renderMenu(className)}
{setMenuMountPoint ? (
<MountPointPortal setMountPoint={setMenuMountPoint}>
{renderMenu(className)}
</MountPointPortal>
) : (
renderMenu(className)
)}
{renderSearchBar()}
</span>
Copy link
Contributor Author

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?

Copy link
Contributor

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.

Comment on lines +165 to +167
// 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);
Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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!

Copy link
Contributor Author

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('');
Copy link
Contributor

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Comment on lines 118 to 127
<span className="kbnTopNavMenu__wrapper">
{renderMenu(className)}
{setMenuMountPoint ? (
<MountPointPortal setMountPoint={setMenuMountPoint}>
{renderMenu(className)}
</MountPointPortal>
) : (
renderMenu(className)
)}
{renderSearchBar()}
</span>
Copy link
Contributor

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.

@pgayvallet pgayvallet removed the request for review from a team August 26, 2020 19:49
@ryankeairns
Copy link
Contributor

@lizozom can we get your review and approval on this one?
It's a dependency for the new stacked header and is blocking some downstream work. Thanks!

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Build metrics

@kbn/optimizer bundle module count

id value diff baseline
kibanaReact 295 +1 294

async chunks size

id value diff baseline
devTools 6.5KB +69.0B 6.5KB

page load bundle size

id value diff baseline
core 1.2MB +2.0KB 1.2MB
kibanaReact 648.1KB +6.0KB 642.1KB
ml 576.7KB -101.0B 576.8KB
navigation 165.5KB +848.0B 164.7KB
total +8.7KB

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@pgayvallet pgayvallet merged commit 981691d into elastic:master Sep 1, 2020
pgayvallet added a commit to pgayvallet/kibana that referenced this pull request Sep 1, 2020
* 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>
@ryankeairns
Copy link
Contributor

YESSSS thanks Pierre!

pgayvallet added a commit that referenced this pull request Sep 1, 2020
* 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>
gmmorris added a commit to gmmorris/kibana that referenced this pull request Sep 2, 2020
* 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)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
REASSIGN from Team:Core UI Deprecated label for old Core UI team release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v7.10.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants