Closed Parkreiner closed 6 months ago
Brain dump from previous message. You don't need to read this unless I suddenly get hit with a bus, in which case, hopefully this is detailed enough
The more I've work with Backstage, the more I've realized that it was a mistake not to use their API factories. When I was first putting the plugin together, I was focused on doing things the "traditional React way", and not the "Backstage way". All the API logic is also thrown into one file, and I think it's getting to the size where it should be split up, now that we've seen a few different use cases play out.
As of this writing, the plugin does not use any custom API factories, which is the main system that Backstage leverages. API Factories are somewhat similar to React Context, in that they're also a dependency injection mechanism, but Backstage is built primarily with factories and not Context. And because the Coder plugin is built mainly with Context, that limits their ability to talk to each other, and let the user swap pieces out when wiring up the plugin.
When wired up correctly, a single API factory consists of three parts. The first two have some pitfalls when trying to show UI state, which I'll highlight in the 'challenges' section below.
The apiRef
value, which basically serves as the "key" for the active class instance. Once a class is instantiated, that instance can be accessed at anytime from the React app by calling the useApi
hook (e.g., useApi(apiRef)
).
Unfortunately, the useApi
name is misleading, as it isn't a React hook at all – it has zero React-specific state. The "hook" grabs the API class instance as a global singleton, and exposes it directly with zero changes.
plugin.ts
file. When you create a plugin with the createPlugin
function, you have the option to feed additional API dependencies in as factories. When defined this way, these factories are always part of the plugin, and cannot be changed by usersapis.ts
file that the Backstage scaffolder automatically generates. When factories are injected this way, the user gets to choose which APIs they want. For example, Backstage offers a number of APIs for working with the most popular source control sites, but on our side, we can define a single coderAuthRef
value, and let the user decide if they want to associate that with token auth or Coder OAuth.Refactoring the codebase to (1) split it up, and (2) use these factories should give us the following benefits:
TokenAuth
class and an OAuth
class, they could use the same coderAuthRef
to let the user decide which auth solution they want to use)Extracting an API factory outside React means that we can start to migrate away from React Context, and sidestep Context's potential performance issues
Up 'til now, I've been very careful about how our CoderProvider's React context has been defined, but there's always a risk that it could still be wired up in a way that causes slow performance. By extracting the main logic outside React, you can make it so that each individual component that cares about the class can subscribe to it individually, without having to go through a shared parent.
deployment
property in the CoderAppConfig
type, and could let us migrate all config logic to be YAML-basedAs mentioned above, the vast majority of API classes are "function buckets". That is, they can be stateful, but:
setState
method that React class components have.Backstage assumes that the vast majority of API calls will be simple on-mount calls that require no unique caching strategy or revalidation/refetching over time. It does not provide any guidance for other use cases.
Backstage's official recommendation for working with API classes is the useAsync
library, which isn't nearly powerful enough for our needs, because it has zero caching/revalidation logic. For example, the current component logic would be outright impossible – we would not be able to automatically detect token changes or workspaces getting added/having their statuses changed.
this
context when they're exposed in the UI (which can happen all too easily if you're not careful).Config
API class in favor of the Discovery
API class. Both would let us take the proxy config values in the main YAML file, and make them available in JavaScript. The big problem, though, is that the Config
API's methods are synchronous, while the Discovery
API's main function is async. We need the values to be available synchronously in UI code, because we're showing parts of the data directly in the UI itself. The Discovery
API was designed with the assumption that it would be used solely for async API calls, so we need some kind of workaround to make the values available synchronouslyBackstage's testing documentation still has a lot of TODO sections, so there isn't any official guidance on using custom API factories, but I know for a fact that you can inject them during testing. It's part of the reason why API factories exist at all. I don't expect this part to take too much time, but I will need to look through the source code to see how the classes from the core Backstage plugins are wiring things up
I already have a separate branch with a lot of the changes already made, but my solution consists of a few parts:
coderAuthApiRef
value and a "shared auth interface" that has methods that could be shared by both a CoderTokenAuth
class, a hypothetical CoderOAuth
class, or any other custom auth solutions we build in the futureCoderTokenAuth
class, and migrate the vast majority of the current auth token logic into that
useSyncExternalStore
hook)this
binding when passed around a React UIuseCoderTokenAuth
hook that handles the tricky steps of wiring up the CoderTokenAuth
class with React Query (particularly its loading/error/success states, and its automatic revalidation)
useCoderTokenAuth
in multiple components, that shouldn't cause anything to blow up, or cause each component that called the hook to fight each other by dispatching different state changes. There should still be one main source of truth (that just happens to be defined outside React)CoderClient
API class and a coderClientApiRef
value for injecting the class into Backstage as a factory
CoderTokenAuth
will, but all public methods should still be defined in a way that ensures they can't lose their this
contextuseCoderClient
custom hook that simplifies wiring up useApi
and coderClientApiRef
for users and plugin developers; make sure the plugin exports itCoderClient
class at the plugin definition level to ensure that it's always defined, and so that the end-user doesn't have to worry about hooking it upcoderAuthRef
and CoderTokenAuth
values, and add user documentation for getting those set up
CoderTokenAuth
or CoderOAuth
to the refUpdate on this: right now, I have a branch that tackles this issue and #114 at once. But with the Coder SDK issues, now both are blocked.
I could split these up, but without the Coder SDK, I don't know if API factories have enough immediate impact to justify undoing some of the work, and then redoing it once the SDK is available. Happy to change course if there's disagreement, but I think this can be parked for a little bit. It won't block other Backstage issues like the Coder buttons
Part of umbrella issue #16 Bleeds into #114 a little bit.
These changes are going to take a while, but they are close to done. Adding API factories should require some slightly complicated upfront work, but should make it easier to add new features over time, while making our plugin align more with modern Backstage community practices.
Requirements
Should have