Skip to content

[Embeddable] Refactor embeddable panel#159837

Merged
ThomThomson merged 35 commits intoelastic:mainfrom
ThomThomson:embeddable/refactorEmbeddablePanel
Jul 17, 2023
Merged

[Embeddable] Refactor embeddable panel#159837
ThomThomson merged 35 commits intoelastic:mainfrom
ThomThomson:embeddable/refactorEmbeddablePanel

Conversation

@ThomThomson
Copy link
Copy Markdown
Contributor

@ThomThomson ThomThomson commented Jun 15, 2023

Summary

This PR accomplishes a number of tasks related to the Embeddable Panel. This PR:

  • Updates the Embeddable panel and all sub-components to be a react component
  • Removes the services props in favour of a simple services provider in the embeddable plugin.
  • Removes the embeddable panel HOC
  • Allows access to the Embeddable panel via direct import rather than via the plugin start contract.
  • Simplifies all usages of the embeddable panel.
  • Shows one unified panel loading state when a panel is loading.

Closes #140408
Closes #148855
Closes #158977

This PR also unblocks #158677 by providing a space where the embeddable migrations can be run client side.

Checklist

Delete any items that are not applicable to this PR.

@ThomThomson
Copy link
Copy Markdown
Contributor Author

@elasticmachine merge upstream

@ThomThomson ThomThomson marked this pull request as ready for review July 7, 2023 19:18
@ThomThomson ThomThomson requested review from a team as code owners July 7, 2023 19:18
@ThomThomson ThomThomson added Team:Presentation Presentation Team for Dashboard, Input Controls, and Canvas t// loe:large Large Level of Effort impact:high Addressing this issue will have a high level of impact on the quality/strength of our product. labels Jul 7, 2023
@ThomThomson
Copy link
Copy Markdown
Contributor Author

@elasticmachine merge upstream

import { distinctUntilKeyChanged } from 'rxjs';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../lib';

export const useSelectFromOptionalEmbeddableInput = <
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not sure useSelectFromOptionalEmbeddableInput is the best name. Its only used once (see code below), and its not for optional key, its for optional embeddable. How about just removing useSelectFromEmbeddableInput and renaming useSelectFromOptionalEmbeddableInput to useSelectFromEmbeddableInput?

const parentHidePanelTitle = useSelectFromOptionalEmbeddableInput(
    'hidePanelTitles',
    embeddable.parent
  );

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's not a bad idea - I think I anticipated using that more. One thing that removing useSelectFromOptionalEmbeddableInput will do is mean that any time you select from the embeddable input it'll be type | undefined. I'm not sure if that would cause any problems - probably will just mean some more guards. I'll implement this!

import React from 'react';
import { EuiLoadingChart, EuiPanel } from '@elastic/eui';

export const EmbeddableLoadingIndicator = ({ showShadow }: { showShadow?: boolean }) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is showShadow prop needed? Its never used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's used once here x-pack/plugins/lens/public/embeddable/embeddable_component.tsx.

import { useSelectFromEmbeddableInput } from '../use_select_from_embeddable';
import { getEditTitleAriaLabel, placeholderTitle } from '../embeddable_panel_strings';

export const useEmbeddablePanelTitle = (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit
Why is this a hook vs just a component? It returns an ReactNode or null. I think making it a normal component vs a hook would make more sense.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call, will swap this over!

Copy link
Copy Markdown
Contributor

@MichaelMarcialis MichaelMarcialis left a comment

Choose a reason for hiding this comment

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

Code review only. Left one small comment below, but nothing worth holding you up over. Approving.

<EuiNotificationBadge
data-test-subj={`embeddablePanelNotification-${notification.id}`}
key={notification.id}
style={{ marginTop: '4px', marginRight: '4px' }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we use EUI variables here instead of the hard-coded 4px?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call! Switched that over!

useEffect(() => {
let subscription: Subscription;

updatePanelActions().then(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This may cause a race condition where useEffect could run again before updatePanelActions returns from the first time useEffect is called. That would set up a subscription that is then never canceled.

You should protect .then path that does not create subscriptions if useEffect is triggered before updatePanelActions returns. This should do the trick

useEffect(() => {
  let ignore = false;
  let subscription: Subscription;

  updatePanelActions().then(() => {
    if (mounted() && !ignore) {
      /**
       * since any piece of state could theoretically change which actions are available we need to
       * recalculate them on any input change or any parent input change.
       */
      subscription = embeddable.getInput$().subscribe(() => updatePanelActions());
      if (embeddable.parent) {
        subscription.add(embeddable.parent.getInput$().subscribe(() => updatePanelActions()));
      }
    }
  });
  return () => {
    ignore = true;
    subscription?.unsubscribe();
  };
}, [embeddable, getAllPanelActions, updatePanelActions, mounted]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good find! I've updated this to match.

className="embPanel__optionsMenuButton"
onClick={() => {
updatePanelActions().then(() => {
if (!mounted()) return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should some kind of loading indicator be displayed? You are delaying reacting to user interaction until an async function finishes. If this async function takes more then 100 milliseconds, then users may think there click was not registered. How about changing the icon to loading indicator, or opening menu and displaying EUI skeleton in place of menu until async action finishes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, why is $input subscription needed if updatePanelActions is called when menu is opened? Wouldn't contextMenuPanels already be current and this call not needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch! In my testing, the async function always returned pretty instantaneously, but it's always best practice to make a proper loading state anyway. I've updated to show this:

Loading Options

IRT the $input subscription - before these changes, the panel actions were recalculated all the time, so I had this subscription to recreate that behaviour. I've removed it in favour of a call on mount, and a call when the menu is opened, and it seems like it works!

@@ -0,0 +1,174 @@
/*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this just be a component instead of a hook?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Probably! Will update that.

embeddable: IEmbeddable;
customizePanelAction?: CustomizePanelAction;
}) => {
const title = embeddable.getTitle();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Parent component EmbeddablePanelHeader also sets up subscriptions for title, viewMode, and description. How about just taking these values as props to avoid duplicating state and $input subscriptions here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I usually like having separate calls to select directly inside the components in which that state gets used. I'm not totally sure if this is a performance issue, but will do some research. I see prop drilling as a bit of a trade-off in that it can occasionally lower readability.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is a best practice for this in the redux style guide called Connect More Components to Read Data from the Store. This isn't exactly the same, because this is using a subscription from RXJS, but the selector uses some sort of subscription under the hood, so I think it might be similar at least.

};

updateNotificationsAndBadges().then(() => {
if (mounted) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also check for canceled to avoid creating subcriptions if useEffect is fired before previous async action completes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This variable actually is tracking the canceled state - you can see that on line 99 - I've just called it mounted here instead. I'll rename the variable to be more consistent though!

Copy link
Copy Markdown
Contributor

@nreese nreese left a comment

Choose a reason for hiding this comment

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

LGTM
code review, tested in chrome

@ThomThomson
Copy link
Copy Markdown
Contributor Author

@elasticmachine merge upstream

@kibana-ci
Copy link
Copy Markdown

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] Defend Workflows Endpoint Cypress Tests #2 / Automated Response Actions From alerts should have generated endpoint and rule

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
embeddable 84 88 +4

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
embeddable 444 434 -10

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
canvas 994.1KB 994.1KB -12.0B
dashboard 356.0KB 355.6KB -401.0B
embeddable 0.0B 11.9KB +11.9KB
securitySolution 9.8MB 9.8MB -12.0B
total +11.5KB

Public APIs missing exports

Total count of every type that is part of your API that should be exported but is not. This will cause broken links in the API documentation system. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats exports for more detailed information.

id before after diff
embeddable 4 7 +3

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
embeddable 76.0KB 64.4KB -11.6KB
lens 36.6KB 36.5KB -74.0B
total -11.6KB
Unknown metric groups

API count

id before after diff
embeddable 548 532 -16

async chunk count

id before after diff
embeddable 0 2 +2

References to deprecated APIs

id before after diff
embeddable 22 16 -6

History

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

Copy link
Copy Markdown
Contributor

@stephmilovic stephmilovic left a comment

Choose a reason for hiding this comment

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

LGTM, tested the security solution implementation and we are all set. Thank you for the updates!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting Feature:Embeddables Relating to the Embeddable system Feature:Embedding Embedding content via iFrame impact:high Addressing this issue will have a high level of impact on the quality/strength of our product. loe:large Large Level of Effort release_note:skip Skip the PR/issue when compiling release notes Team:Presentation Presentation Team for Dashboard, Input Controls, and Canvas t// v8.10.0

Projects

None yet

8 participants