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

Cleanup spaces plugin #91976

Merged
merged 27 commits into from
Mar 1, 2021
Merged

Cleanup spaces plugin #91976

merged 27 commits into from
Mar 1, 2021

Conversation

jportner
Copy link
Contributor

@jportner jportner commented Feb 19, 2021

Paying down technical debt on the Spaces plugin:

  • Cleans up imports, reordering/consolidating them and enforcing usage of the import type directive
  • Lazy-loads components, including reusable components that are exposed to consumers
  • Removes static export of SpaceAvatar component

@jportner jportner added v8.0.0 release_note:skip Skip the PR/issue when compiling release notes v7.13.0 labels Feb 19, 2021
@jportner jportner force-pushed the cleanup-spaces-plugin branch 3 times, most recently from 83015b5 to 27ed077 Compare February 19, 2021 22:20
@jportner
Copy link
Contributor Author

@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:

Async chunks

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

id before after diff
ml 6.4MB 6.4MB +594.0B
savedObjectsManagement 163.8KB 164.1KB +266.0B
total +860.0B

Page load bundle

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

id before after diff
spaces 274.7KB 275.5KB +771.0B
spacesOss 4.5KB 4.6KB +105.0B
total +876.0B

@legrego
Copy link
Member

legrego commented Feb 22, 2021

@jportner I haven't looked at the changes in this PR yet (so feel free to ignore this comment), but we might need to also incorporate the changes I started in #91018 in order to see page load improvements.

I'll try to circle back to this in the next day or so to take a closer look

@jportner
Copy link
Contributor Author

So, here are the metrics for Phase 2.5 after we added the reusable UI components:

Async chunks

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

id before after diff
ml 6.4MB 6.3MB -14.4KB
savedObjectsManagement 163.7KB 164.1KB +356.0B
total -14.1KB

Page load bundle

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

id before after diff
ml 68.4KB 68.2KB -233.0B
spaces 231.3KB 274.9KB +43.6KB
spacesOss 3.5KB 4.5KB +1.0KB
total +44.5KB

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.

@legrego
Copy link
Member

legrego commented Feb 22, 2021

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.
@jportner
Copy link
Contributor Author

OK, after a bunch of tweaks I've managed to get the page load bundle size cut down dramatically.

We had 29kb of transitive dependencies imported from @kbn/std, I was able to remove that too.

image

@jportner jportner marked this pull request as ready for review February 23, 2021 03:56
@jportner jportner requested a review from a team as a code owner February 23, 2021 03:56
@@ -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);
Copy link
Contributor

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

Copy link
Contributor

@pgayvallet pgayvallet Feb 24, 2021

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.

Copy link
Contributor Author

@jportner jportner Feb 25, 2021

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 />}>
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

@jportner jportner Feb 24, 2021

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

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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 in React.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.

Copy link
Contributor

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.
@jportner jportner added the auto-backport Deprecated - use backport:version if exact versions are needed label Feb 25, 2021
@jportner jportner enabled auto-merge (squash) February 25, 2021 18:38
Copy link
Contributor

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

getStartServices().then(([coreStart]) => {
setNotifications(coreStart.notifications);
});
});
Copy link
Contributor

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

Copy link
Contributor Author

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/ui_api/lazy_wrapper.tsx Outdated Show resolved Hide resolved
@jportner
Copy link
Contributor Author

jportner commented Mar 1, 2021

@elasticmachine merge upstream

Copy link
Contributor

@thomheymann thomheymann left a 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);
Copy link
Contributor

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);

Copy link
Contributor Author

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 🙈

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
spaces 270 259 -11

Async chunks

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

id before after diff
ml 6.4MB 6.4MB +229.0B
savedObjectsManagement 163.8KB 163.8KB -20.0B
security 723.8KB 724.3KB +484.0B
spaces 41.5KB 309.3KB ⚠️ +267.8KB
total +268.5KB

Page load bundle

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

id before after diff
security 130.9KB 131.0KB +104.0B
spaces 274.7KB 44.6KB -230.1KB
spacesOss 4.5KB 4.7KB +212.0B
total -229.8KB
Unknown metric groups

async chunk count

id before after diff
spaces 1 10 +9

History

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

@jportner jportner merged commit 8710a81 into elastic:master Mar 1, 2021
@kibanamachine
Copy link
Contributor

💔 Backport failed

❌ 7.x: Commit could not be cherrypicked due to conflicts

To backport manually, check out the target branch and run:
node scripts/backport --pr 91976

@jportner jportner deleted the cleanup-spaces-plugin branch March 1, 2021 16:16
jportner added a commit that referenced this pull request Mar 1, 2021
* 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 <42973632+kibanamachine@users.noreply.github.com>
jloleysens added a commit that referenced this pull request Mar 3, 2021
… 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-backport Deprecated - use backport:version if exact versions are needed backported release_note:skip Skip the PR/issue when compiling release notes v7.13.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants