-
Notifications
You must be signed in to change notification settings - Fork 5
refactor: reorganize API logic and create class/hook for simplifying proxy logic #124
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
Conversation
const queryIsEnabled = authToken !== ''; | ||
const authValidityQuery = useQuery<boolean>({ | ||
queryKey: [...sharedAuthQueryKey, authToken], | ||
enabled: queryIsEnabled, | ||
keepPreviousData: queryIsEnabled, | ||
refetchOnWindowFocus: query => query.state.data !== false, | ||
queryFn: async () => { | ||
// In this case, the request doesn't actually matter. Just need to make any | ||
// kind of dummy request to validate the auth | ||
const requestInit = await getCoderApiRequestInit(authToken, identityApi); | ||
const apiEndpoint = await urlSyncApi.getApiEndpoint(); | ||
const response = await fetch(`${apiEndpoint}/users/me`, requestInit); | ||
|
||
if (response.status >= 400 && response.status !== 401) { | ||
throw new BackstageHttpError('Failed to complete request', response); | ||
} | ||
|
||
return response.status !== 401; | ||
}, |
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.
Moved the auth query options factory function here to consolidate the logic more (especially since this is the only file that needs it), and then, to make setting up the function itself easier, I inlined everything
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.
The logic here gets simplified down a lot more with the CoderClient
PR, where the entire value of queryFn
becomes () => coderClient.syncToken(authToken)
type TempPublicUrlSyncApi = Readonly<{ | ||
getApiEndpoint: UrlSync['getApiEndpoint']; | ||
getAssetsEndpoint: UrlSync['getAssetsEndpoint']; | ||
}>; |
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.
Hoping to get rid of this in the next PR. I just couldn't without making the CoderClient
work start bleeding into this PR
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.
Removed as of the next PR
/** | ||
* @todo This is a temporary property that is being used until the | ||
* CoderClientApi is created, and can consume the UrlSync class directly. | ||
* | ||
* Delete this entire property once the new class is ready. | ||
*/ | ||
api: Readonly<{ | ||
getApiEndpoint: UrlSync['getApiEndpoint']; | ||
getAssetsEndpoint: UrlSync['getAssetsEndpoint']; | ||
}>; |
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, hoping to get rid of this with the next PR
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.
Also now removed via the new CoderClient
PR
getCachedUrls: () => UrlSyncSnapshot; | ||
}>; | ||
|
||
export class UrlSync implements UrlSyncApi { |
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 is sync as in synchronous, right, because you can access the URLs synchronously? Not worth blocking over the name, but it does seem at odds with how there are async methods on the class, which look like the main usages of the class except for isEmojiUrl
which uses assetsRoute
.
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.
Just about to log off, so I won't be looking at the rest of the comments until tomorrow morning, but really quick: it's actually "sync" as in "synchronization"
The main goal of the class is synchronizing React with the Backstage APIs (and also working with useSyncExternalStore
, which also uses "sync" in the "synchronizing" sense)
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.
Could probably rename things to remove that ambiguity
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.
Ahhhhhhhh I probably had the ...Sync
methods in Node's fs
on my brain. No really good alternatives come to my mind, so I am not opposed to just leaving it.
// ConfigApi is literally only used because it offers a synchronous way to | ||
// get an initial URL to use from inside the constructor. Should not be used | ||
// beyond initial constructor call | ||
private readonly configApi: ConfigApi; |
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.
My instinct is that if we only need one synchronous property off configApi
in the constructor, we might as well make that clear in the parameters as well, and just pass in a string for the base URL rather than the entire API (easier to mock too, I guess). Unless we might need something else off it later. Not a blocker, passing the entire thing does make it easier to use too, so maybe not worth changing.
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.
Yeah, I see what you mean. My main worry is that from all the Backstage examples I've seen, they've received the entire API directly, so there might be value in keeping consistency with that, especially if we start getting open-source contributions
I also feel like if a user of the API is required to pass in only a string, they're more at risk of passing in the wrong one. If we defer that logic to Backstage, and keep things centralized in UrlSync
, that helps improve the odds that the URLs will never be wrong. If they are, that'll be a bug with Backstage's main code
Luckily, the entire ConfigApi
is basically a one-method interface, so it won't ever be hard to mock out, but one thing I can do in the meantime is split the difference. As in, receive the configApi
value from the constructor inputs, but only use it for the constructor call, and never embed it
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.
Fair!
const proxyRoot = await this.discoveryApi.getBaseUrl( | ||
PROXY_URL_KEY_FOR_DISCOVERY_API, | ||
); |
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.
Tangential to this PR, but I still think we should consider making queries directly to the Coder API and remove the proxy altogether.
Backstage docs do say this makes senses under circumstances that appear to apply to us, and the reasons for doing it via a proxy do not seem relevant to us: https://backstage.io/docs/plugins/call-existing-api#issuing-requests-directly
But, the reason I bring it up now is that it would eliminate the need for both configApi
and discoveryApi
, I think, at least as they are used here, so it would affect this new class. We only need the URL on the Coder config object, if I recall correctly.
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.
Yeah, that you mention it, as long as we know the single endpoint, and since we're in control of how it's structured, I don't think there would be any issues with ditching the proxy. It would also remove the need for the user to update their app-config.yaml
file
I'm going to keep the code the same as-is for now, just because there's two other PRs depending on it, and because even if this isn't the final iteration, the API code does get consolidated a lot more. The current code should make it easier to remove the proxy and extra APIs down the line
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.
Makes sense to me!
); | ||
|
||
const newSnapshot = this.prepareNewSnapshot(proxyRoot); | ||
this.urlCache.updateSnapshot(newSnapshot); |
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.
If you have to call these methods to update the snapshot, you would always need to call these methods, right? Which means you should already have the endpoint you need, and have no need for state
.
If we had something like discoveryApi.subscribe(() => urlCache.update())
, then it would make more sense to me, because then we would not need the methods at all, the snapshot would always be up to date, and we could just access it synchronously off useUrlSync().state
(no idea if discoveryApi
has that, just thinking out loud).
I see the only place we access the synchronous state is isEmojiUrl
which uses assetsRoute
, but IMO we could just remove that function and remove the border radius. The Coder dashboard does not use any rounding, which I think makes sense; even some of the non-emoji icons we use for templates are meant to be square or have tips that get cut off.
tl;dr maybe we do not need any snapshot in this class if we get rid of the one current usage? Although, then it becomes a very thin wrapper around configApi
and discoveryApi
, and maybe it will make sense to call them directly.
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.
Yeah, a subscription would be nice, and I think that would ultimately be the best design – I'm just not completely sure what the best way to implement it with the current APIs would be. DiscoveryApi
doesn't have a subscription method
As far as the rounding, though, that was basically an aesthetic decision. If I look at my Coder dashboard right now:
I think the images look a little sloppy, because there's no "aesthetic consistency" between them. They're not using the same colors, and by default, don't have the same shape, so the only thing giving them visual rhythm is the fact that they're placed consistently in the same spot. Enforcing rounding increases their similarities, and makes them look like they're supposed to be similar. I actually think removing the rounding would make make the UI look less polished
I've actually been meaning to open a PR for Coder core to update the CSS to get the emoji rounding issues fixed, too
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 might make sense to leave being consistent up to the folks uploading the icons, as frustrating as that is. If you check out the templates page, some of the non-emoji icons are non-circle shapes which I think look weird or wrong when they are forcefully rounded.
I think this is why we have the padding to emojis, to avoid cutting them off, but regular images can have the same problem. How do we tell the difference between an emoji or emoji-like image and one that can be rounded?
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.
Some would look better rounded though, maybe it makes sense to add a "Round this icon?" option to the template settings, otherwise folks have to manually round their icons which seems annoying. Although, it seems like this mostly only happens when the image is kinda bad to begin with, and includes a background color. And sometimes they contain extra space making them overly small which I guess there is not much we can do about except ask the user if they want to cut off some of the image or something.
Also sometimes it seems we display a square person icon (on the dashboard, not sure what would happen in Backstage), which I think happens when the image does not load? Seems like we should put something else there.
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.
Yeah, good point – I could go either way, honestly
I feel like user avatars (and other images that are basically for UI purposes) kind of have the expectation that they can be morphed or changed into any number of shapes, and that if the image the user supplies doesn't look good with the styling the vendor/app uses, it's the user's responsibility to fix that. I can't remember the last time I've seen a modern site not use some amount of rounding with their "UI-ish" images. But I don't think workspaces/templates quite have that expectation
With the emoji, we at least know that they will always come from the same source (I don't think we give users the ability to define custom emoji), so they're all working with the same uniform constraints and can be offset the same consistent way
Realistically, though, most icons are going to have their main content in the center, so they naturally lend themselves to being rounded. I think I'd rather choose making 95% of icons look better, with 5% looking worse, over 100% looking "okay-ish". I'm not sure if this is worth exposing another property to the end user, 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.
And the plugin does have logic to handle there not being any images, too – those use the same rounding
If we get rid of the rounding for the images, I'm not sure what to do with the fallbacks. I guess we could keep those rounded, but jumping from non-rounded to rounded might be weird. And even if we don't completely remove the rounding, and just reduce it, I think the UI still starts to look a little funny
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 feel like user avatars (and other images that are basically for UI purposes) kind of have the expectation that they can be morphed or changed into any number of shapes [...] But I don't think workspaces/templates quite have that expectation
I think that expectation can make sense for images, but not as much for icons. We use mostly emojis and logos for icons, but I wonder if that is the case generally for other users.
Tangentially, the current devcontainers icon feels odd to me because it has some built-in padding making it look too small in comparison. I tried a different icon without the padding so it matches the other icons, what do you think?
With the emoji, we at least know that they will always come from the same source (I don't think we give users the ability to define custom emoji), so they're all working with the same uniform constraints and can be offset the same consistent way
A custom emoji could be added like any other icon but yeah no way to modify the default set of emojis pretty sure, unless maybe you build from source with your own emojis.
From playing around on the dashboard, we would need 5px padding to prevent emojis from getting cropped, and that makes them look unusually small compared to the other icons. Is it just me though?
Realistically, though, most icons are going to have their main content in the center, so they naturally lend themselves to being rounded. I think I'd rather choose making 95% of icons look better, with 5% looking worse, over 100% looking "okay-ish".
With the bundled icons and the ones we have added at least, we might make more look worse than better (going off the templates page and https://dev.coder.com/icons), but that might not hold true for icons added by other users.
I think it is not so much about being centered (which I think all ours are), but about being able to fit in the bounds of a circle. For example the Vault triangle turns into a slice of pizza, JFrog on the templates page becomes IFrog with a mangled g
, Kotlin turns into Pac-Man 😂 and Fedora loses its little tail.
And the plugin does have logic to handle there not being any images, too – those use the same rounding
Ah I was thinking about the case where there is an image, but it is a 404.
And even if we don't completely remove the rounding, and just reduce it, I think the UI still starts to look a little funny
I actually quite like the decreased rounding on those fallbacks!! But I also like Lisp so who knows if I can be trusted. The letters alone without a background, maybe extra bold, could look cool too. Good topic for FE variety maybe?
I probably sound really passionate about this (maybe I am lol) but I mostly feel motivated to remove it because it means the entire snapshot and subscription code in this class goes away. I think it felt like a lot to me for all that code only to support rounding! 😛
To add one more argument to the small book I have written here, we display these icons in Backstage, the dashboard, and the Gateway plugin; right now Backstage is the odd one out.
// Defined here and not in CoderAuthProvider.ts to avoid circular dependency | ||
// issues |
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 is the song that never ends 🎶
// Disabling query object when there is no query text for performance reasons; | ||
// searching through every workspace with an empty string can be incredibly | ||
// slow. | ||
const enabled = inputs.auth.isAuthenticated && inputs.coderQuery !== ''; |
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.
Tangential to the PR since it has been like this for a while, but should we do:
const enabled = inputs.auth.isAuthenticated && inputs.coderQuery !== ''; | |
const enabled = inputs.auth.isAuthenticated && inputs.coderQuery.trim() !== ''; |
Then again, I have been using a space to query all workspaces for testing so I kinda like that I can do that.
On the flip side, maybe we should mimic the dashboard behavior which allows blank input. Maybe not until after we use the new parameters endpoint though, so we are not blasting off hundreds of queries. 😆
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.
Oh, good call – I can add trim
really quickly
throw new BackstageHttpError('Failed to complete request', response); | ||
} | ||
|
||
return response.status !== 401; |
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 boolean indicates whether the token is valid, right?
- Maybe safer to explicitly check for 20x rather than not 401 (I know we return on >=400 above, but not sure if we can get 30x codes, maybe if there is some kind of infinite redirect misconfiguration, and maybe there are very weird cases where a misconfiguration returns a 10x for some reason, and who knows what other codes might be added in the future).
- That the response includes a user, to make sure we really did hit the deployment and not just some other thing that returns a 20x (for example suppose there is temporarily an HTML page here that returns 200 because the deployment is down for maintenance).
Not a blocker though since this is the status quo!
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.
Yeah, good call – I think it'll be easier to make this change for the CoderClient
PR, so I'll make the changes there
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.
Whoops, meant to approve. Looks good to me, but I do think UrlSync
might not be necessary depending on what we decide about the proxy.
Think I finally have things figured out. This is part 1 of a new set of PRs for bringing the Coder SDK into Backstage.
Connected to #107
Changes made
I'd say most of these changes qualify as me cleaning up previous messes, and making it easier to build on the plugin going forward.
api.ts
file up:/api/api.ts
- Contains the majority of the previous fetch logic. This will eventually becomeCoderClient
/api/errors.ts
- Contains custom error definitions/api/queryOptions.ts
- Contains the query option factories that should be used in multiple places throughout the codebaseCoderAuthProvider.tsx
UrlSync
class for managing URLs a lot more consistentlyDiscoveryApi
, and is designed to work withuseSyncExternalStore
CoderClient
needs to make a request, it will get it throughUrlSync
.UrlSync
will then detect when the API endpoints change and notify React components through the hookDiscoveryApi
, except that we have better React UI supportuseBackstageEndpoints
hook intouseUrlSync
Notes
DiscoveryApi
jank right now wasn't worth it, but the more I thought about it, the more I realized that it would fix a lot of problemsCoderClient
– a lot of the properties I was originally trying to expose through it were better suited forUrlSync
CoderClient
, and then the Coder SDK