[Embeddable] Refactor embeddable panel#159837
Conversation
…EmbeddablePanel' into embeddable/refactorEmbeddablePanel
…test. Restore seamless React integration. Remove check before updating badges, notifications, and actions.
|
@elasticmachine merge upstream |
|
@elasticmachine merge upstream |
…EmbeddablePanel' into embeddable/refactorEmbeddablePanel
| import { distinctUntilKeyChanged } from 'rxjs'; | ||
| import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../lib'; | ||
|
|
||
| export const useSelectFromOptionalEmbeddableInput = < |
There was a problem hiding this comment.
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
);
There was a problem hiding this comment.
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 }) => { |
There was a problem hiding this comment.
Is showShadow prop needed? Its never used.
There was a problem hiding this comment.
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 = ( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Good call, will swap this over!
MichaelMarcialis
left a comment
There was a problem hiding this comment.
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' }} |
There was a problem hiding this comment.
Should we use EUI variables here instead of the hard-coded 4px?
There was a problem hiding this comment.
Good call! Switched that over!
| useEffect(() => { | ||
| let subscription: Subscription; | ||
|
|
||
| updatePanelActions().then(() => { |
There was a problem hiding this comment.
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]);
There was a problem hiding this comment.
Good find! I've updated this to match.
| className="embPanel__optionsMenuButton" | ||
| onClick={() => { | ||
| updatePanelActions().then(() => { | ||
| if (!mounted()) return; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
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!
…EmbeddablePanel' into embeddable/refactorEmbeddablePanel
| @@ -0,0 +1,174 @@ | |||
| /* | |||
There was a problem hiding this comment.
Should this just be a component instead of a hook?
There was a problem hiding this comment.
Probably! Will update that.
| embeddable: IEmbeddable; | ||
| customizePanelAction?: CustomizePanelAction; | ||
| }) => { | ||
| const title = embeddable.getTitle(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Also check for canceled to avoid creating subcriptions if useEffect is fired before previous async action completes
There was a problem hiding this comment.
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!
nreese
left a comment
There was a problem hiding this comment.
LGTM
code review, tested in chrome
|
@elasticmachine merge upstream |
💛 Build succeeded, but was flaky
Failed CI StepsTest Failures
Metrics [docs]Module Count
Public APIs missing comments
Async chunks
Public APIs missing exports
Page load bundle
Unknown metric groupsAPI count
async chunk count
References to deprecated APIs
History
To update your PR or re-run it, just comment with: |
stephmilovic
left a comment
There was a problem hiding this comment.
LGTM, tested the security solution implementation and we are all set. Thank you for the updates!

Summary
This PR accomplishes a number of tasks related to the Embeddable Panel. This PR:
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.