-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Cleanup spaces plugin #91976
Cleanup spaces plugin #91976
Conversation
83015b5
to
27ed077
Compare
@legrego I added lazy-loading for our reusable UI components in 27ed077 but it did not positively impact the page load bundle for the SOM and ML plugins. Perhaps we only get the performance benefit if we statically import components? Snapshot of the Kibana Machine comment:
|
So, here are the metrics for Phase 2.5 after we added the reusable UI components:
The ML and SOM bundles never increased, but the Spaces bundle did. So I guess what you're saying here is that our goal is to decrease the Spaces page load bundle? That makes sense. I'll see if I can tinker with it a bit more. |
Yes my goal was to decrease the Spaces page load bundle. I wasn't concerned about the ML/SOM bundles here |
This is needed to use the import sorter
Sort order 1. External dependencies 2. Internal absolute dependencies (@kbn/..., src/...) 3. Internal relative dependencies Also cleaned up some relative dependencies in the process.
Adds ESLint rule to enforce this.
Splits out most components into separate chunks
An error was getting logged to the console because jsdom does not support window.location.reload() out of the box.
Rename it from 'CopySavedObjectsToSpaceFlyout' to 'CopyToSpaceFlyoutInternal'.
Rename it from 'SpaceAvatar' to 'SpaceAvatarInternal'
This was statically imported by the Security plugin so it required quite a bit of refactoring.
Reduces page load bundle by 29KB
Thought this would decrease the page load bundle size, but it only shaved off a few hundred bytes. At any rate, this change cleans up the code a bit, and will be needed when we eventually expose this as a reusable component for outside consumers.
27ed077
to
b8d93b0
Compare
@@ -66,7 +66,9 @@ export const JobSpacesList: FC<Props> = ({ spacesApi, spaceIds, jobId, jobType, | |||
}); | |||
} | |||
|
|||
const { SpaceList, ShareToSpaceFlyout } = spacesApi.ui.components; | |||
const LazySpaceList = React.lazy(spacesApi.ui.components.getSpaceList); | |||
const LazyShareToSpaceFlyout = React.lazy(spacesApi.ui.components.getShareToSpaceFlyout); |
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.
Dynamic imports using React.lazy
should be created outside the render function, otherwise you're creating a new component with every render of JobSpacesList
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.
There isn't really anything 'outside' of render
in a FC 😅 . But useMemo
should be used around the React.lazy
declaration, yea (as it was properly done in saved_objects_table_page.tsx
)
Highly depends on the answers on #91976 (comment) though.
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 just pushed some changes.
Wherever I could do so (inside the Spaces plugin) I pulled out the lazy loading to the top level.
Otherwise, I memoized these inside of function components.
@@ -81,11 +83,11 @@ export const JobSpacesList: FC<Props> = ({ spacesApi, spaceIds, jobId, jobType, | |||
}; | |||
|
|||
return ( | |||
<> | |||
<React.Suspense fallback={<EuiLoadingSpinner />}> |
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.
Suspense will throw an error if the lazy component cannot be loaded so you might want to wrap this in an error boundary and create a fallback.
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.
This component is only rendered if the Spaces plugin is enabled and the Spaces API is available, and the getters to lazy load these components (getSpaceList
, getShareToSpaceFlyout
) are just promises to import the components that we are already guaranteed to have available. IMO we don't need to complicate this with an error boundary and fallback when we can rely on the Kibana platform's public contracts.
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 discussed offline -- this is necessary if the user runs into any network errors at load time 😅 I'll add wrappers for these that display a toast message so the whole page doesn't blow up.
Edit: done in 032b5cb.
spacesApi ? spacesApi.ui.components.getSpacesContext : getEmptyFunctionComponent | ||
), | ||
[spacesApi] | ||
); |
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 probably don't have enough context (lol, see what I did there?) so might be better to catch up tomorrow but it would be nice to be a bit clearer here in regards to naming.
getSpacesContext
to me sounds like we're getting a React Context object for spaces. That's usually a static object which contains a Provider and a Consumer component. In this case however, we are wrapping it in React.lazy
to create a lazy loaded component so it's just the Provider component by the looks of it? In that case it might be clearer to call the method getSpacesProvider
and the return value SpacesProvider
.
Having said that, Context (returned by createContext
) is usually a static object in React so we could just make that available through the spaces plugin contract directly rather than having these getter functions and having to memoize them.
Anyways, let's catch up tomorrow, just adding this as a reminder.
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.
Yea, I share the same concerns. React context providers are usually static, and the value they provide mutates. I'm not sure to understand why we need an lazy accessor around a context provider?
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.
it would be nice to be a bit clearer here in regards to naming.
I'm open to naming changes!
getSpacesContext
to me sounds like we're getting a React Context object for spaces. That's usually a static object which contains a Provider and a Consumer component. In this case however, we are wrapping it inReact.lazy
to create a lazy loaded component so it's just the Provider component by the looks of it?
Sort of. It's a "wrapper" component that includes the context provider. We need this because when this wrapper is mounted, it makes a series of API calls and caches the results, then makes those available to its children via the context provider.
The function to obtain this wrapper within the Spaces plugin is called getSpacesContextWrapper
, but the one that is exposed to consumers in the public contract is called getSpacesContext
. We could rename the latter to something like getSpacesContextProviderWrapper
but I don't know what value that really adds for consumers, it's an implementation detail IMO. They don't need to consume the context provider, they just need to know that they have to render the flyout, space list, etc. as a descendant of this wrapper.
Having said that, Context (returned by
createContext
) is usually a static object in React so we could just make that available through the spaces plugin contract directly rather than having these getter functions and having to memoize them.
I don't think so, we need the wrapper.
I'm not sure to understand why we need an lazy accessor around a context provider?
Because it's not just a context provider, it's the wrapper that also fetches data. Also, to flip that question around: why not provide a lazy accessor? All of the other components have lazy accessors. It seems this approach is more consistent.
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.
Caught up with Joe and I can't think of a good alternative to avoid the architectural complexity with the approach here.
To fulfil the brief, we need a way to make the spaces state available globally, so that we can provide drop-in components for developers that share that state. However, we're not able to render the context provider at the root level of the application ourselves (since spaces is a plugin) and we can't provide static exports (due to the restriction imposed by the platform), hence the indirection using dynamically created and memoized context providers, that require developers to consume them using the spaces plugin contract rather than static imports like EUI.
I'm guessing this is a bit of an edge case and other plugin developers don't have the same requirements to share core functionality like spaces does. However, I would argue that certain things like i18n, routing, session information, authenticated user information and in this particular case spaces are global concerns that should be available throughout the application. Usually these are managed at the root level in React applications so that any child component has access via hooks and context but this isn't really possible with the way we've currently modelled spaces as a plugin.
Happy to leave this as is for now but I am slightly worried about the complexity and maintainability of the current solution.
Components exposed in the public contract are now wrapped and automatically lazy-loaded. Consumers no longer have to handle this.
If we have an exception where we do not use an error boundary, I added a comment explaining why. I also standardized our usages of `lazy` and `Suspense` by destructuring the React import.
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.
Looks good, couple of comments related to your usage of hooks.
...ublic/management/roles/edit_role/privileges/kibana/privilege_summary/space_column_header.tsx
Show resolved
Hide resolved
x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx
Outdated
Show resolved
Hide resolved
...k/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx
Show resolved
Hide resolved
...k/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx
Show resolved
Hide resolved
getStartServices().then(([coreStart]) => { | ||
setNotifications(coreStart.notifications); | ||
}); | ||
}); |
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.
Again, the side effect here will run at every render unless you add a dependency array and you also need to ensure the component is still mounted when modifying state asynchronously.
What you could do so you don't have to manage all that yourself is use the useAsync
hook from react-use
module:
const { value: { notifications } } = useAsync(getStartServices); // `value` is the `coreStart` object returned by `getStartServices`
return <SuspenseErrorBoundary notifications={notifications} />
https://streamich.github.io/react-use/?path=/story/side-effects-useasync--docs
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.
Good idea, thanks!
Destructuring makes this a bit funky, but this is what I did to make it work:
const { value: startServices = [{ notifications: undefined }] } = useAsync(getStartServices);
const [{ notifications }] = startServices;
x-pack/plugins/spaces/public/suspense_error_boundary/suspense_error_boundary.tsx
Show resolved
Hide resolved
@elasticmachine merge upstream |
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.
Looks gooooood!!!
setNotifications(coreStart.notifications); | ||
}); | ||
}); | ||
const { value: startServices = [{ notifications: undefined }] } = useAsync(getStartServices); |
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.
For next time: An empty object should be enough here:
const { value: startServices = [{}] } = useAsync(getStartServices);
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.
Ah, I did this because there's no type overlap between CoreStart
and {}
, so tsc complains about that. Using { notifications: undefined }
allows for type inferencing. Now that you mention it though I suppose this would work
const { value: startServices = [{} as CoreStart] } = useAsync(getStartServices);
I think it's a bit better to have explicit types but we've got a big mix of inferences + explicit typing everywhere 🙈
💚 Build Succeeded
Metrics [docs]Module Count
Async chunks
Page load bundle
History
To update your PR or re-run it, just comment with: |
💔 Backport failed❌ 7.x: Commit could not be cherrypicked due to conflicts To backport manually, check out the target branch and run: |
* Cleanup spaces plugin (#91976) # Conflicts: # x-pack/plugins/security/public/management/roles/roles_management_app.tsx * Make the linter happy Co-authored-by: Kibana Machine <[email protected]>
… ilm/rollup-v2-action * 'ilm/rollup-v2-action' of github.com:elastic/kibana: [Security Solution][Case][Bug] Only add rule object for alert comments (#92977) [Security Solution][Case] Show the current connector name in case view (#93018) [Security Solution] Remove unused mock data (#92357) Adds mapping to the signals for the indicator rules that were missing (#92928) skip flaky suite (#85208) Cleanup spaces plugin (#91976) Control round and decimal places in Gauge Visualization when using aggregate functions like average (#91293) Added alerting ui mock for jest test (#92604) Remove "beta" label from URL Drilldown as it is now GA (#92859)
Paying down technical debt on the Spaces plugin:
import type
directiveSpaceAvatar
component