Skip to content

refactor: consolidate API logic into CoderClient class #125

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

Merged
merged 66 commits into from
Apr 30, 2024
Merged

Conversation

Parkreiner
Copy link
Member

@Parkreiner Parkreiner commented Apr 29, 2024

Closes #107 (at least until we need to figure out a good shared auth solution)

Changes made

  • Redefined the previous API logic so that it's all behind a CoderClient class
    • Also defined a stub/mock CoderSdk class that the CoderClient relies on, so that it will be easier to bring in the real Coder SDK down the line. The thinking is that it will be a matter of swapping the mock for the real thing
    • Defined client to consume Backstage APIs directly, reducing the amount of APIs that needed to be grabbed from within the React UI, and manually passed around for the API calls
  • Made a new useCoderSdk hook
    • We aren't exporting this to the end user just yet, but it will be easy to do that once the full SDK is brought in
    • Main point is making us start dogfooding it for the rest of the UI components
  • Centralized some of the mock server endpoint logic

@Parkreiner Parkreiner requested a review from code-asher April 29, 2024 16:43
@Parkreiner Parkreiner self-assigned this Apr 29, 2024
Copy link
Member

@code-asher code-asher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun to see it come together!

this.trackedEjectionIds.delete(ejectionId);
}

return sizeBeforeRemoval !== this.trackedEjectionIds.size;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very much a nit haha, but why a size check over return true in the if otherwise return false?

Another nit, since this is only used in one spot, is it worth a method? Could maybe just have eject there and not add/remove to the tracked IDs at all, since the finally will always remove it anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good call on the size check – your suggestion's a lot more straightforward

As for whether the method makes sense, I guess my gut feeling is that if I'm going to have a special add method for the interceptors, it makes sense to have an obvious way to remove them, too. Plus, since it's only a private method, it'd be easy to remove down the line, too

Comment on lines +167 to +171
// Manually aborting a request is always treated as an error, even if we
// 100% expect it. Just scrub the error if it's from the cleanup
if (errorIsFromCleanup) {
return undefined;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this cause calls not to error? Like if I was using this in my own project, and called client.getWorkspaces() and it got aborted, would it return with undefined?

If so, part of me wants a more explicit Go-like syntax to check if the error is an abort error (errors.Is(err, context.Canceled)) but at the same time, maybe this is React-y, and as long as the editor tells me "undefined means aborted" when I look at the function definition, that would not be too unreasonable.

Copy link
Member Author

@Parkreiner Parkreiner Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, as far as I can tell from the Axios docs, the way the error interceptor works is that, Axios will run all the interceptors, and then if the value it gets back is not undefined, Axios will treat that value as an error and throw it (even if the value isn't an Error instance). I don't think it's inherently React-ish – it's strictly an Axios thing

I could've just had return, but Backstage's linters complain about mixing return and return <value> in the same function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yup, I get that the return undefined here is causing Axios to not throw and not being used itself as the return of client.getWorkspaces(), but I wonder what now is the return of client.getWorkspaces() with the error having been scrubbed? Since it did not throw, but there was also no data because it aborted, is it just naturally undefined maybe?

And if so, maybe returning undefined for canceled things is React-like so this is fine, was my thinking, but honestly I have no idea. 😛 So you would write code like:

const workspaces = client.getWorkspaces()
if (workspaces === undefined) {
  // This means the request was canceled
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, yeah, good point – realistically, I only expect the cleanupClient function to be used for testing, but that is a hole in the logic that could pause pain points down the line

It's not the worst thing in the world – React Query exposes the error property as type unknown by default to be defensive, so technically, all RQ code already has to deal with an "error" potentially being undefined. But if I do need to keep this method for connecting to the Coder core files, I'll be sure to figure something out

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah good point, this really only comes into play when cleaning up the client.

// The Axios docs have incredibly confusing wording about how multiple
// interceptors work. They say the interceptors are "run in the order
// added", implying that the first interceptor you add will always run
// first. That is not true - they're run in reverse order, so the newer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes that is confusing, good catch!

Comment on lines +2 to +3
* @file This is a temporary (and significantly limited) implementation of the
* "Coder SDK" that will eventually be imported from Coder core
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this approach!

Comment on lines 184 to 185
const originalGetWorkspaces = baseSdk.getWorkspaces;
baseSdk.getWorkspaces = async request => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this override temporary or here to stay? I wonder if we override it with the return below instead, like:

const getWorkspaces = () {}
// stuff
{
 ...baseSdk,
  getWorkspaces,
}

Copy link
Member Author

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 doing it that way is more clear

Comment on lines +277 to +285
const templateIconUrl = ws.template_icon;
if (!templateIconUrl.startsWith('/')) {
return ws;
}

return {
...ws,
template_icon: `${assetsRoute}${templateIconUrl}`,
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly just musing, but if we did not have a proxy we could use URL to resolve the base:

Suggested change
const templateIconUrl = ws.template_icon;
if (!templateIconUrl.startsWith('/')) {
return ws;
}
return {
...ws,
template_icon: `${assetsRoute}${templateIconUrl}`,
};
return {
...ws,
template_icon: ws.template_icon ? new URL(ws.template_icon, deploymentURL).href : undefined,
}

Comment on lines +314 to +315
const tokenIsInvalid =
err instanceof AxiosError && err.response?.status === 401;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To piggy back off my comment in the other PR, we should probably validate the response data.

Does AxiosError cover all non-20x status codes? I thought maybe it only covered failures to make the request.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed – going to fix really quick

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I looked this up, and the answer is "it depends". By default, AxiosError does cover all non-2xx status codes. But you can add a validateStatus function to the axios.get call to customize your logic for how you treat the status code

So right now, CoderClient isn't doing that, and neither is Coder Core. But once we bring in the core files, things could always change if we're not careful

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooooooooooooohhh right now that you say that, I remember seeing validateStatus in Coder core. That makes sense.

// Actual request type doesn't matter; just need to make some kind of
// dummy request. Should favor requests that all users have access to and
// that don't require request bodies
await this.sdk.getUserLoginType();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getUserLoginType does seem reasonable to me, but in our other plugins we get the user; maybe it could be unclear why we do it differently in one spot? Could be cool to cache that user info at some point too in case we want to show the user's name or something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, for some reason, when I was looking at the functions in the api.ts file, I somehow missed getAuthenticatedUser

I wanted to mirror the function calls that we have there, but if we still have this function, I definitely think it makes more sense to use that

}
};

cleanupClient = (): void => {
Copy link
Member

@code-asher code-asher Apr 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this how thorough this is with the cleanup logic, but the client is meant to last for the lifetime of the application, right? Is there a case where you would need to recreate the client? I wonder if we should write it with the assumption it is long-lived, at least until/unless there is a use case.

Copy link
Member Author

@Parkreiner Parkreiner Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's sort of a carry over from the previous PR I closed out. There, it actually was necessary to have cleanup logic, because the file was operating under the assumption that we'd be importing a single Axios instance from the Coder core files, and you needed a way to ensure isolation for Client instances spun up for different test cases. Right now, CoderClient gets a brand new Axios instance each time it's instantiated, but that may or may not have to change, based on what I can change in core

So at least for right now, I kind of threw it in here just because. The actual CoderClient in production should basically be a singleton that never gets removed (especially since it's not tied to React rendering, so it can't get conditionally unmounted). Just to be on the cautious side, I'm going to hold onto the method for this PR, and then depending on how much luck I have with the Coder core changes to improve its test isolation, I'll either:

  1. Remove this method because I'll be able to get guaranteed isolation from Coder core
  2. Beef up the method because getting isolation set up in Core is harder than I thought

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfection!


for (const agent of resource.agents) {
const status = agent.status;
if (!uniqueStatuses.includes(status)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a cool use case for Set.

Base automatically changed from mes/api-v2-01 to main April 30, 2024 16:09
@Parkreiner Parkreiner merged commit 74e7dd3 into main Apr 30, 2024
4 checks passed
@Parkreiner Parkreiner deleted the mes/api-v2-02 branch April 30, 2024 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Coder plugin: refactor codebase to use API factories
2 participants