diff --git a/plugins/backstage-plugin-coder/docs/README.md b/plugins/backstage-plugin-coder/docs/README.md index 1aac4a05..95019233 100644 --- a/plugins/backstage-plugin-coder/docs/README.md +++ b/plugins/backstage-plugin-coder/docs/README.md @@ -1,11 +1,22 @@ -# Plugin API Reference – Coder for Backstage +# Documentation Directory – `backstage-plugin-coder` v0.3.0 -For users who need more information about how to extend and modify the Coder plugin. For general setup, please see our main [README](../README.md). +This document lists core information for the Backstage Coder plugin. It is intended for users who have already set up the plugin and are looking to take it further. -All documentation reflects version `v0.2.0` of the plugin. Note that breaking API changes may continue to happen for minor versions until the plugin reaches version `v1.0.0`. +For general setup, please see our [main README](../README.md). -## Documentation directory +## Documentation listing -- [Components](./components.md) -- [Custom React hooks](./hooks.md) -- [Important types](./types.md) +### Guides + +- [Using the Coder API from Backstage](./guides/coder-api.md) + - [Advanced use cases for the Coder API](./guides//coder-api-advanced.md) + +### API reference + +- [Components](./api-reference/components.md) +- [Custom React hooks](./api-reference/hooks.md) +- [Important types](./api-reference/types.md) + +## Notes about semantic versioning + +We fully intend to follow semantic versioning with the Coder plugin for Backstage. Expect some pain points as we figure out the right abstractions needed to hit version 1, but we will try to minimize breaking changes as much as possible as the library gets ironed out. diff --git a/plugins/backstage-plugin-coder/docs/catalog-info.md b/plugins/backstage-plugin-coder/docs/api-reference/catalog-info.md similarity index 100% rename from plugins/backstage-plugin-coder/docs/catalog-info.md rename to plugins/backstage-plugin-coder/docs/api-reference/catalog-info.md diff --git a/plugins/backstage-plugin-coder/docs/components.md b/plugins/backstage-plugin-coder/docs/api-reference/components.md similarity index 100% rename from plugins/backstage-plugin-coder/docs/components.md rename to plugins/backstage-plugin-coder/docs/api-reference/components.md diff --git a/plugins/backstage-plugin-coder/docs/hooks.md b/plugins/backstage-plugin-coder/docs/api-reference/hooks.md similarity index 100% rename from plugins/backstage-plugin-coder/docs/hooks.md rename to plugins/backstage-plugin-coder/docs/api-reference/hooks.md diff --git a/plugins/backstage-plugin-coder/docs/types.md b/plugins/backstage-plugin-coder/docs/api-reference/types.md similarity index 100% rename from plugins/backstage-plugin-coder/docs/types.md rename to plugins/backstage-plugin-coder/docs/api-reference/types.md diff --git a/plugins/backstage-plugin-coder/docs/guides/coder-api-advanced.md b/plugins/backstage-plugin-coder/docs/guides/coder-api-advanced.md new file mode 100644 index 00000000..fb90ebe6 --- /dev/null +++ b/plugins/backstage-plugin-coder/docs/guides/coder-api-advanced.md @@ -0,0 +1,72 @@ +# Working with the Coder API - advanced use cases + +This guide covers some more use cases that you can leverage for more advanced configuration of the Coder API from within Backstage. + +## Changing fallback auth component behavior + +By default, `CoderProvider` is configured to display a fallback auth UI component when two cases are true: + +1. The user is not authenticated +2. There are no official Coder components are being rendered to the screen. + +The Coder auth fallback UI + +All official Coder plugin components are configured to let the user add auth information if the user isn't already authenticated, so the fallback component only displays when there would be no other way to add the information. + +However, depending on your use cases, `CoderProvider` can be configured to change how it displays the fallback, based on the value of the `fallbackAuthUiMode` prop. + +```tsx + + + +``` + +There are three values that can be set for the mode: + +- `restrained` (default) - The auth fallback will only display if the user is not authenticated, and there would be no other way for the user to add their auth info. +- `assertive` - The auth fallback will always display when the user is not authenticated, regardless of what Coder component are on-screen. But the fallback will **not** appear if the user is authenticated. +- `hidden` - The auth fallback will never appear under any circumstances. Useful if you want to create entirely custom components and don't mind wiring your auth logic manually via `useCoderAuth`. + +## Connecting a custom query client to the Coder plugin + +By default, the Coder plugin uses and manages its own query client. This works perfectly well if you aren't using React Query for any other purposes, but if you are using it throughout your Backstage deployment, it can cause issues around redundant state (e.g., not all cached data being vacated when the user logs out). + +To prevent this, you will need to do two things: + +1. Pass in your custom React Query query client into the `CoderProvider` component +2. "Group" your queries with the Coder query key prefix + +```tsx +const yourCustomQueryClient = new QueryClient(); + + + +; + +// Ensure that all queries have the correct query key prefix +import { useQuery } from '@tanstack/react-react-query'; +import { + CODER_QUERY_KEY_PREFIX, + useCoderQuery, +} from '@coder/backstage-plugin-coder'; + +function CustomComponent() { + const query1 = useQuery({ + queryKey: [CODER_QUERY_KEY_PREFIX, 'workspaces'], + queryFn: () => { + // Get workspaces here + }, + }); + + // useCoderQuery automatically prefixes all query keys with + // CODER_QUERY_KEY_PREFIX if it's not already the first value of the array + const query2 = useCoderQuery({ + queryKey: ['workspaces'], + queryFn: () => { + // Get workspaces here + }, + }); + + return
Main component content
; +} +``` diff --git a/plugins/backstage-plugin-coder/docs/guides/coder-api.md b/plugins/backstage-plugin-coder/docs/guides/coder-api.md new file mode 100644 index 00000000..04e8d10d --- /dev/null +++ b/plugins/backstage-plugin-coder/docs/guides/coder-api.md @@ -0,0 +1,262 @@ +# Coder API - Quick-start guide + +## Overview + +The Coder plugin makes it easy to bring the entire Coder API into your Backstage deployment. This guide covers how to get it set up so that you can start accessing Coder from Backstage. + +Note: this covers the main expected use cases with the plugin. For more information and options on customizing your Backstage deployment further, see our [Advanced API guide](./coder-api-advanced.md). + +### Before you begin + +Please ensure that you have the Coder plugin fully installed before proceeding. You can find instructions for getting up and running in [our main README](../../README.md). + +### Important hooks for using the Coder API + +The Coder plugin exposes three (soon to be four) main hooks for accessing Coder plugin state and making queries/mutations + +- `useCoderAuth` - Provides methods and state values for interacting with your current Coder auth session from within Backstage. + + ```tsx + function SessionTokenInputForm() { + const [sessionTokenDraft, setSessionTokenDraft] = useState(''); + const coderAuth = useCoderAuth(); + + const onSubmit = (event: FormEvent) => { + coderAuth.registerNewToken(sessionToken); + setSessionTokenDraft(''); + }; + + return ( +
+ + + ); + } + ``` + +- `useCoderQuery` - Makes it simple to query data from the Coder API and share it throughout your application. + + ```tsx + function WorkspacesList() { + // Return type matches the return type of React Query's useQuerys + const workspacesQuery = useCoderQuery({ + queryKey: ['workspaces'], + queryFn: ({ coderApi }) => coderApi.getWorkspaces({ limit: 5 }), + }); + } + ``` + +- `useCoderMutation` (coming soon) - Makes it simple to mutate data via the Coder API. +- `useCoderApi` - Exposes an object with all available Coder API methods. None of the state in this object is tied to React render logic - it can be treated as a "function bucket". Once `useCoderMutation` is available, the main value of this hook will be as an escape hatch in the rare situations where `useCoderQuery` and `useCoderMutation` don't meet your needs. Under the hood, both `useCoderQuery` and `useCoderMutation` receive their `coderApi` context value from this hook. + + ```tsx + function HealthCheckComponent() { + const coderApi = useCoderApi(); + + const processWorkspaces = async () => { + const workspacesResponse = await coderApi.getWorkspaces({ + limit: 10, + }); + + processHealthChecks(workspacesResponse.workspaces); + }; + } + ``` + +Internally, the Coder plugin uses [React Query/TanStack Query v4](https://tanstack.com/query/v4/docs/framework/react/overview). In fact, `useCoderQuery` and `useCoderMutation` are simply wrappers over `useQuery` and `useMutation`. Both simplify the process of wiring up the hooks' various properties to the Coder auth, while exposing a more convenient way of accessing the Coder API object. + +If you ever need to coordinate queries and mutations, you can use `useQueryClient` from React Query - no custom plugin-specific hook needed. + +The bottom of this document has examples of both queries and mutations. + +### Grouping queries with the Coder query key prefix + +The plugin exposes a `CODER_QUERY_KEY_PREFIX` constant that you can use to group all Coder queries. `useCoderQuery` automatically injects this value into all its `queryKey` arrays. However, if you need to escape out with `useQuery`, you can import the constant and manually include it as the first value of your query key. + +In addition, all official Coder plugin components use this prefix internally. + +```tsx +// All grouped queries can be invalidated at once from the query client +const queryClient = useQueryClient(); +const invalidateAllCoderQueries = () => { + queryClient.invalidateQuery({ + queryKey: [CODER_QUERY_KEY_PREFIX], + }); +}; + +// The prefix is only needed when NOT using useCoderQuery +const customQuery = useQuery({ + queryKey: [CODER_QUERY_KEY_PREFIX, 'workspaces'], + queryFn: () => { + // Your custom API logic + }, +}); + +// When the user unlinks their session token, all queries grouped under +// CODER_QUERY_KEY_PREFIX are vacated from the active query cache +function LogOutButton() { + const { unlinkToken } = useCoderAuth(); + + return ( + + ); +} +``` + +## Recommendations for accessing the API + +1. If querying data, prefer `useCoderQuery`. It automatically wires up all auth logic to React Query (which includes pausing queries if the user is not authenticated). It also lets you access the Coder API via its query function. `useQuery` works as an escape hatch if `useCoderQuery` doesn't meet your needs, but it requires more work to wire up correctly. +2. If mutating data, you will need to call `useMutation`, `useQueryClient`, and `useCoderApi` in tandem\*. + +We highly recommend **not** fetching with `useState` + `useEffect`, or with `useAsync`. Both face performance issues when trying to share state. See [ui.dev](https://www.ui.dev/)'s wonderful [_The Story of React Query_ video](https://www.youtube.com/watch?v=OrliU0e09io) for more info on some of the problems they face. + +\* `useCoderMutation` can be used instead of all three once that hook is available. + +### Comparing query caching strategies + +| | `useAsync` | `useQuery` | `useCoderQuery` | +| ------------------------------------------------------------------ | ---------- | ---------- | --------------- | +| Automatically handles race conditions | ✅ | ✅ | ✅ | +| Can retain state after component unmounts | 🚫 | ✅ | ✅ | +| Easy, on-command query invalidation | 🚫 | ✅ | ✅ | +| Automatic retry logic when a query fails | 🚫 | ✅ | ✅ | +| Less need to fight dependency arrays | 🚫 | ✅ | ✅ | +| Easy to share state for sibling components | 🚫 | ✅ | ✅ | +| Pre-wired to Coder auth logic | 🚫 | 🚫 | ✅ | +| Can consume Coder API directly from query function | 🚫 | 🚫 | ✅ | +| Automatically groups Coder-related queries by prefixing query keys | 🚫 | 🚫 | ✅ | + +## Authentication + +All API calls to **any** of the Coder API functions will fail if you have not authenticated yet. Authentication can be handled via any of the official Coder components that can be imported via the plugin. However, if there are no Coder components on the screen, the `CoderProvider` component will automatically\* inject a fallback auth button for letting the user add their auth info. + +https://github.com/coder/backstage-plugins/assets/28937484/0ece4410-36fc-4b32-9223-66f35953eeab + +Once the user has been authenticated, all Coder API functions will become available. When the user unlinks their auth token (effectively logging out), all cached queries that start with `CODER_QUERY_KEY_PREFIX` will automatically be vacated. + +\* This behavior can be disabled. Please see our [advanced API guide](./coder-api-advanced.md) for more information. + +## Component examples + +Here are some full code examples showcasing patterns you can bring into your own codebase. + +Note: To keep the examples simple, none of them contain any CSS styling or MUI components. + +### Displaying recent audit logs + +```tsx +import React from 'react'; +import { useCoderQuery } from '@coder/backstage-plugin-coder'; + +function RecentAuditLogsList() { + const auditLogsQuery = useCoderQuery({ + queryKey: ['audits', 'logs'], + queryFn: ({ coderApi }) => coderApi.getAuditLogs({ limit: 10 }), + }); + + return ( + <> + {auditLogsQuery.isLoading &&

Loading…

} + {auditLogsQuery.error instanceof Error && ( +

Encountered the following error: {auditLogsQuery.error.message}

+ )} + + {auditLogsQuery.data !== undefined && ( +
    + {auditLogsQuery.data.audit_logs.map(log => ( +
  • {log.description}
  • + ))} +
+ )} + + ); +} +``` + +## Creating a new workspace + +Note: this example showcases how to perform mutations with `useMutation`. The example will be updated once `useCoderMutation` is available. + +```tsx +import React, { type FormEvent, useState } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { + type CreateWorkspaceRequest, + CODER_QUERY_KEY_PREFIX, + useCoderQuery, + useCoderApi, +} from '@coder/backstage-plugin-coder'; + +export function WorkspaceCreationForm() { + const [newWorkspaceName, setNewWorkspaceName] = useState(''); + const coderApi = useCoderSdk(); + const queryClient = useQueryClient(); + + const currentUserQuery = useCoderQuery({ + queryKey: ['currentUser'], + queryFn: coderApi.getAuthenticatedUser, + }); + + const workspacesQuery = useCoderQuery({ + queryKey: ['workspaces'], + queryFn: coderApi.getWorkspaces, + }); + + const createWorkspaceMutation = useMutation({ + mutationFn: (payload: CreateWorkspaceRequest) => { + if (currentUserQuery.data === undefined) { + throw new Error( + 'Cannot create workspace without data for current user', + ); + } + + const { organization_ids, id: userId } = currentUserQuery.data; + return coderApi.createWorkspace(organization_ids[0], userId, payload); + }, + }); + + const onSubmit = async (event: FormEvent) => { + event.preventDefault(); + + // If the mutation fails, useMutation will expose the error in the UI via + // its own exposed properties + await createWorkspaceMutation.mutateAsync({ + name: newWorkspaceName, + }); + + setNewWorkspaceName(''); + queryClient.invalidateQueries({ + queryKey: [CODER_QUERY_KEY_PREFIX, 'workspaces'], + }); + }; + + return ( + <> + {createWorkspaceMutation.isSuccess && ( +

+ Workspace {createWorkspaceMutation.data.name} created successfully! +

+ )} + +
+
+ Required fields + + +
+ + +
+ + ); +} +``` diff --git a/plugins/backstage-plugin-coder/screenshots/auth-fallback.png b/plugins/backstage-plugin-coder/screenshots/auth-fallback.png new file mode 100644 index 00000000..d5b817cc Binary files /dev/null and b/plugins/backstage-plugin-coder/screenshots/auth-fallback.png differ diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts index 2bfa6b24..db9a7275 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts @@ -1,4 +1,4 @@ -import { CODER_AUTH_HEADER_KEY, CoderClient } from './CoderClient'; +import { CODER_AUTH_HEADER_KEY, CoderClientWrapper } from './CoderClient'; import type { IdentityApi } from '@backstage/core-plugin-api'; import { UrlSync } from './UrlSync'; import { rest } from 'msw'; @@ -34,10 +34,10 @@ function getConstructorApis(): ConstructorApis { return { urlSync, identityApi }; } -describe(`${CoderClient.name}`, () => { +describe(`${CoderClientWrapper.name}`, () => { describe('syncToken functionality', () => { it('Will load the provided token into the client if it is valid', async () => { - const client = new CoderClient({ apis: getConstructorApis() }); + const client = new CoderClientWrapper({ apis: getConstructorApis() }); const syncResult = await client.syncToken(mockCoderAuthToken); expect(syncResult).toBe(true); @@ -50,12 +50,12 @@ describe(`${CoderClient.name}`, () => { }), ); - await client.sdk.getAuthenticatedUser(); + await client.api.getAuthenticatedUser(); expect(serverToken).toBe(mockCoderAuthToken); }); it('Will NOT load the provided token into the client if it is invalid', async () => { - const client = new CoderClient({ apis: getConstructorApis() }); + const client = new CoderClientWrapper({ apis: getConstructorApis() }); const syncResult = await client.syncToken('Definitely not valid'); expect(syncResult).toBe(false); @@ -68,12 +68,12 @@ describe(`${CoderClient.name}`, () => { }), ); - await client.sdk.getAuthenticatedUser(); + await client.api.getAuthenticatedUser(); expect(serverToken).toBe(null); }); it('Will propagate any other error types to the caller', async () => { - const client = new CoderClient({ + const client = new CoderClientWrapper({ // Setting the timeout to 0 will make requests instantly fail from the // next microtask queue tick requestTimeoutMs: 0, @@ -96,13 +96,13 @@ describe(`${CoderClient.name}`, () => { }); }); - // Eventually the Coder SDK is going to get too big to test every single + // Eventually the Coder API is going to get too big to test every single // function. Focus tests on the functionality specifically being patched in // for Backstage - describe('Coder SDK', () => { + describe('Coder API', () => { it('Will remap all workspace icon URLs to use the proxy URL if necessary', async () => { const apis = getConstructorApis(); - const client = new CoderClient({ + const client = new CoderClientWrapper({ apis, initialToken: mockCoderAuthToken, }); @@ -126,7 +126,7 @@ describe(`${CoderClient.name}`, () => { }), ); - const { workspaces } = await client.sdk.getWorkspaces({ + const { workspaces } = await client.api.getWorkspaces({ q: 'owner:me', limit: 0, }); @@ -142,12 +142,12 @@ describe(`${CoderClient.name}`, () => { }); it('Lets the user search for workspaces by repo URL', async () => { - const client = new CoderClient({ + const client = new CoderClientWrapper({ initialToken: mockCoderAuthToken, apis: getConstructorApis(), }); - const { workspaces } = await client.sdk.getWorkspacesByRepo( + const { workspaces } = await client.api.getWorkspacesByRepo( { q: 'owner:me' }, mockCoderWorkspacesConfig, ); diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.ts index 4c5333dd..c760f1d2 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.ts @@ -7,23 +7,23 @@ import { CODER_API_REF_ID_PREFIX } from '../typesConstants'; import type { UrlSync } from './UrlSync'; import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; import { - type CoderSdk, + type CoderApi, type User, type Workspace, type WorkspacesRequest, type WorkspacesResponse, - makeCoderSdk, + createCoderApi, } from './vendoredSdk'; export const CODER_AUTH_HEADER_KEY = 'Coder-Session-Token'; const DEFAULT_REQUEST_TIMEOUT_MS = 20_000; /** - * A version of the main Coder SDK API, with additional Backstage-specific + * A version of the main Coder API, with additional Backstage-specific * methods and properties. */ -export type BackstageCoderSdk = Readonly< - CoderSdk & { +export type BackstageCoderApi = Readonly< + CoderApi & { getWorkspacesByRepo: ( request: WorkspacesRequest, config: CoderWorkspacesConfig, @@ -31,8 +31,8 @@ export type BackstageCoderSdk = Readonly< } >; -type CoderClientApi = Readonly<{ - sdk: BackstageCoderSdk; +type CoderClientWrapperApi = Readonly<{ + api: BackstageCoderApi; /** * Validates a new token, and loads it only if it is valid. @@ -75,7 +75,7 @@ type RequestInterceptor = ( config: RequestConfig, ) => RequestConfig | Promise; -export class CoderClient implements CoderClientApi { +export class CoderClientWrapper implements CoderClientWrapperApi { private readonly urlSync: UrlSync; private readonly identityApi: IdentityApi; @@ -84,7 +84,7 @@ export class CoderClient implements CoderClientApi { private readonly trackedEjectionIds: Set; private loadedSessionToken: string | undefined; - readonly sdk: BackstageCoderSdk; + readonly api: BackstageCoderApi; constructor(inputs: ConstructorInputs) { const { @@ -100,7 +100,7 @@ export class CoderClient implements CoderClientApi { this.cleanupController = new AbortController(); this.trackedEjectionIds = new Set(); - this.sdk = this.createBackstageCoderSdk(); + this.api = this.createBackstageCoderApi(); this.addBaseRequestInterceptors(); } @@ -108,7 +108,7 @@ export class CoderClient implements CoderClientApi { requestInterceptor: RequestInterceptor, errorInterceptor?: (error: unknown) => unknown, ): number { - const axios = this.sdk.getAxiosInstance(); + const axios = this.api.getAxiosInstance(); const ejectionId = axios.interceptors.request.use( requestInterceptor, errorInterceptor, @@ -121,7 +121,7 @@ export class CoderClient implements CoderClientApi { private removeRequestInterceptorById(ejectionId: number): boolean { // Even if we somehow pass in an ID that hasn't been associated with the // Axios instance, that's a noop. No harm in calling method no matter what - const axios = this.sdk.getAxiosInstance(); + const axios = this.api.getAxiosInstance(); axios.interceptors.request.eject(ejectionId); if (!this.trackedEjectionIds.has(ejectionId)) { @@ -181,11 +181,11 @@ export class CoderClient implements CoderClientApi { this.addRequestInterceptor(baseRequestInterceptor, baseErrorInterceptor); } - private createBackstageCoderSdk(): BackstageCoderSdk { - const baseSdk = makeCoderSdk(); + private createBackstageCoderApi(): BackstageCoderApi { + const baseApi = createCoderApi(); - const getWorkspaces: (typeof baseSdk)['getWorkspaces'] = async request => { - const workspacesRes = await baseSdk.getWorkspaces(request); + const getWorkspaces: (typeof baseApi)['getWorkspaces'] = async request => { + const workspacesRes = await baseApi.getWorkspaces(request); const remapped = await this.remapWorkspaceIconUrls( workspacesRes.workspaces, ); @@ -214,7 +214,7 @@ export class CoderClient implements CoderClientApi { q: appendParamToQuery(request.q, key, stringUrl), }; - return baseSdk.getWorkspaces(patchedRequest); + return baseApi.getWorkspaces(patchedRequest); }), ); @@ -237,7 +237,7 @@ export class CoderClient implements CoderClientApi { }; return { - ...baseSdk, + ...baseApi, getWorkspaces, getWorkspacesByRepo, }; @@ -312,7 +312,7 @@ export class CoderClient implements CoderClientApi { // 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 - const dummyUser = await this.sdk.getAuthenticatedUser(); + const dummyUser = await this.api.getAuthenticatedUser(); // Most of the time, we're going to trust the types returned back from the // server without doing any type-checking, but because this request does @@ -376,6 +376,6 @@ function assertValidUser(value: unknown): asserts value is User { } } -export const coderClientApiRef = createApiRef({ +export const coderClientWrapperApiRef = createApiRef({ id: `${CODER_API_REF_ID_PREFIX}.coder-client`, }); diff --git a/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts b/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts index 62001e4e..00e86a7c 100644 --- a/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts +++ b/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts @@ -5,7 +5,7 @@ import { getMockDiscoveryApi, mockBackstageAssetsEndpoint, mockBackstageUrlRoot, - mockBackstageApiEndpointWithoutSdkPath, + mockBackstageApiEndpointWithoutVersionSuffix, } from '../testHelpers/mockBackstageData'; // Tests have to assume that DiscoveryApi and ConfigApi will always be in sync, @@ -23,7 +23,7 @@ describe(`${UrlSync.name}`, () => { const cachedUrls = urlSync.getCachedUrls(); expect(cachedUrls).toEqual({ baseUrl: mockBackstageUrlRoot, - apiRoute: mockBackstageApiEndpointWithoutSdkPath, + apiRoute: mockBackstageApiEndpointWithoutVersionSuffix, assetsRoute: mockBackstageAssetsEndpoint, }); }); diff --git a/plugins/backstage-plugin-coder/src/api/queryOptions.ts b/plugins/backstage-plugin-coder/src/api/queryOptions.ts index 6bfbd800..b622e415 100644 --- a/plugins/backstage-plugin-coder/src/api/queryOptions.ts +++ b/plugins/backstage-plugin-coder/src/api/queryOptions.ts @@ -1,7 +1,7 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import type { Workspace, WorkspacesRequest } from './vendoredSdk'; import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; -import type { BackstageCoderSdk } from './CoderClient'; +import type { BackstageCoderApi } from './CoderClient'; import type { CoderAuth } from '../components/CoderProvider'; // Making the type more broad to hide some implementation details from the end @@ -47,13 +47,13 @@ function getSharedWorkspacesQueryKey(coderQuery: string) { type WorkspacesFetchInputs = Readonly<{ auth: CoderAuth; - sdk: BackstageCoderSdk; + api: BackstageCoderApi; coderQuery: string; }>; export function workspaces({ auth, - sdk, + api, coderQuery, }: WorkspacesFetchInputs): UseQueryOptions { const enabled = auth.isAuthenticated; @@ -64,7 +64,7 @@ export function workspaces({ keepPreviousData: enabled && coderQuery !== '', refetchInterval: getCoderWorkspacesRefetchInterval, queryFn: async () => { - const res = await sdk.getWorkspaces({ + const res = await api.getWorkspaces({ q: coderQuery, limit: 0, }); @@ -82,7 +82,7 @@ type WorkspacesByRepoFetchInputs = Readonly< export function workspacesByRepo({ coderQuery, - sdk, + api, auth, workspacesConfig, }: WorkspacesByRepoFetchInputs): UseQueryOptions { @@ -98,7 +98,7 @@ export function workspacesByRepo({ refetchInterval: getCoderWorkspacesRefetchInterval, queryFn: async () => { const request: WorkspacesRequest = { q: coderQuery, limit: 0 }; - const res = await sdk.getWorkspacesByRepo(request, workspacesConfig); + const res = await api.getWorkspacesByRepo(request, workspacesConfig); return res.workspaces; }, }; diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts index bf293267..6877a614 100644 --- a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts @@ -1894,7 +1894,7 @@ function getConfiguredAxiosInstance(): AxiosInstance { } else { // Do not write error logs if we are in a FE unit test. if (process.env.JEST_WORKER_ID === undefined) { - // eslint-disable-next-line no-console -- Function should never run in vendored version of SDK + // eslint-disable-next-line no-console -- Function should never run in vendored version of API console.error('CSRF token not found'); } } diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts index f8451116..18fc9eae 100644 --- a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts @@ -29,8 +29,8 @@ type PropertyToHide = // with the extra properties omitted). But because classes are wonky and exist // as both runtime values and types, it didn't seem possible, even with things // like class declarations. Making a new function is good enough for now, though -export type CoderSdk = Omit; -export function makeCoderSdk(): CoderSdk { +export type CoderApi = Omit; +export function createCoderApi(): CoderApi { const api = new Api(); - return api as CoderSdk; + return api as CoderApi; } diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthDistrustedForm.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthDistrustedForm.tsx index a37c1916..3f58804d 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthDistrustedForm.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthDistrustedForm.tsx @@ -36,7 +36,7 @@ export const CoderAuthDistrustedForm = () => {

Unable to verify token authenticity. Please check your internet - connection, or try ejecting the token. + connection, or try unlinking the token.

diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.test.tsx index 95ce2993..79b263ca 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.test.tsx @@ -16,12 +16,12 @@ type RenderInputs = Readonly<{ }>; async function renderAuthWrapper({ authStatus }: RenderInputs) { - const ejectToken = jest.fn(); + const unlinkToken = jest.fn(); const registerNewToken = jest.fn(); const auth: CoderAuth = { ...mockAuthStates[authStatus], - ejectToken, + unlinkToken, registerNewToken, }; @@ -40,7 +40,7 @@ async function renderAuthWrapper({ authStatus }: RenderInputs) { , ); - return { ...renderOutput, ejectToken, registerNewToken }; + return { ...renderOutput, unlinkToken, registerNewToken }; } describe(`${CoderAuthForm.name}`, () => { @@ -70,18 +70,18 @@ describe(`${CoderAuthForm.name}`, () => { } }); - it('Lets the user eject the current token', async () => { - const { ejectToken } = await renderAuthWrapper({ + it('Lets the user unlink the current token', async () => { + const { unlinkToken } = await renderAuthWrapper({ authStatus: 'distrusted', }); const user = userEvent.setup(); - const ejectButton = await screen.findByRole('button', { + const unlinkButton = await screen.findByRole('button', { name: /Unlink Coder account/, }); - await user.click(ejectButton); - expect(ejectToken).toHaveBeenCalled(); + await user.click(unlinkButton); + expect(unlinkToken).toHaveBeenCalled(); }); }); diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx index 63b9fdd0..efc23329 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx @@ -19,7 +19,7 @@ export function UnlinkAccountButton({ ...delegatedProps }: Props) { const styles = useStyles(); - const { ejectToken } = useInternalCoderAuth(); + const { unlinkToken } = useInternalCoderAuth(); return ( { - ejectToken(); + unlinkToken(); onClick?.(event); }} {...delegatedProps} diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index 33b5bc0a..9b4eb549 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -24,7 +24,7 @@ import { CODER_QUERY_KEY_PREFIX, sharedAuthQueryKey, } from '../../api/queryOptions'; -import { coderClientApiRef } from '../../api/CoderClient'; +import { coderClientWrapperApiRef } from '../../api/CoderClient'; import { CoderLogo } from '../CoderLogo'; import { CoderAuthFormDialog } from '../CoderAuthFormDialog'; @@ -67,7 +67,7 @@ export type CoderAuth = Readonly< AuthState & { isAuthenticated: boolean; registerNewToken: (newToken: string) => void; - ejectToken: () => void; + unlinkToken: () => void; } >; @@ -91,7 +91,7 @@ function useAuthState(): CoderAuth { const [readonlyInitialAuthToken] = useState(authToken); const [isInsideGracePeriod, setIsInsideGracePeriod] = useState(true); - const coderClient = useApi(coderClientApiRef); + const coderClient = useApi(coderClientWrapperApiRef); const queryIsEnabled = authToken !== ''; const authValidityQuery = useQuery({ @@ -149,12 +149,14 @@ function useAuthState(): CoderAuth { // Pseudo-mutex; makes sure that if we get a bunch of errors, only one // revalidation will be processed at a time - let isRevalidatingToken = false; + let isRevalidating = false; const revalidateTokenOnError = async (event: QueryCacheNotifyEvent) => { const queryError = event.query.state.error; + const shouldRevalidate = - !isRevalidatingToken && + isAuthenticated && + !isRevalidating && BackstageHttpError.isInstance(queryError) && queryError.status === 401; @@ -162,9 +164,9 @@ function useAuthState(): CoderAuth { return; } - isRevalidatingToken = true; + isRevalidating = true; await queryClient.refetchQueries({ queryKey: sharedAuthQueryKey }); - isRevalidatingToken = false; + isRevalidating = false; }; const queryCache = queryClient.getQueryCache(); @@ -178,7 +180,7 @@ function useAuthState(): CoderAuth { } }, []); - const ejectToken = useCallback(() => { + const unlinkToken = useCallback(() => { setAuthToken(''); window.localStorage.removeItem(TOKEN_STORAGE_KEY); queryClient.removeQueries({ queryKey: [CODER_QUERY_KEY_PREFIX] }); @@ -188,7 +190,7 @@ function useAuthState(): CoderAuth { ...authState, isAuthenticated, registerNewToken, - ejectToken, + unlinkToken, }; } @@ -275,7 +277,7 @@ export function useInternalCoderAuth(): CoderAuth { /** * Exposes Coder auth state to the rest of the UI. */ -// This hook should only be used by end users trying to use the Coder SDK inside +// This hook should only be used by end users trying to use the Coder API inside // Backstage. The hook is renamed on final export to avoid confusion export function useEndUserCoderAuth(): CoderAuth { const authContextValue = useContext(AuthStateContext); @@ -625,7 +627,7 @@ type AuthFallbackProvider = FC< // Matches each behavior for the fallback auth UI to a specific provider. This // is screwy code, but by doing this, we ensure that if the user chooses not to -// have a dynamic auth fallback UI, their app will have far less tracking logic, +// have dynamic a auth fallback UI, their app will have far less tracking logic, // meaning less performance overhead and fewer re-renders from something the // user isn't even using const fallbackProviders = { diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx index 382917d8..b58af930 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx @@ -27,7 +27,10 @@ import { renderHookAsCoderEntity, } from '../../testHelpers/setup'; import { UrlSync, urlSyncApiRef } from '../../api/UrlSync'; -import { CoderClient, coderClientApiRef } from '../../api/CoderClient'; +import { + CoderClientWrapper, + coderClientWrapperApiRef, +} from '../../api/CoderClient'; describe(`${CoderProvider.name}`, () => { describe('AppConfig', () => { @@ -66,7 +69,7 @@ describe(`${CoderProvider.name}`, () => { apis: { discoveryApi, configApi }, }); - const coderClientApi = new CoderClient({ + const coderClientApi = new CoderClientWrapper({ apis: { urlSync, identityApi }, }); @@ -80,7 +83,7 @@ describe(`${CoderProvider.name}`, () => { [discoveryApiRef, discoveryApi], [urlSyncApiRef, urlSync], - [coderClientApiRef, coderClientApi], + [coderClientWrapperApiRef, coderClientApi], ]} > { }); }; - it('Should let the user eject their auth token', async () => { + it('Should let the user unlink their auth token', async () => { const { result } = renderUseCoderAuth(); act(() => result.current.registerNewToken(mockCoderAuthToken)); @@ -109,7 +112,7 @@ describe(`${CoderProvider.name}`, () => { ); }); - act(() => result.current.ejectToken()); + act(() => result.current.unlinkToken()); expect(result.current).toEqual( expect.objectContaining>({ diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx index fd562851..079e1f38 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx @@ -45,8 +45,8 @@ const defaultClient = new QueryClient({ export const CoderProvider = ({ children, appConfig, - queryClient = defaultClient, fallbackAuthUiMode = 'restrained', + queryClient = defaultClient, }: CoderProviderProps) => { return ( diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.test.tsx index 008d931a..d170db36 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.test.tsx @@ -29,8 +29,11 @@ type RenderInputs = Readonly<{ }>; async function renderButton({ buttonText }: RenderInputs) { - const ejectToken = jest.fn(); - const auth: CoderAuth = { ...mockAuthStates.authenticated, ejectToken }; + const unlinkToken = jest.fn(); + const auth: CoderAuth = { + ...mockAuthStates.authenticated, + unlinkToken: unlinkToken, + }; /** * Pretty sure there has to be a more elegant and fault-tolerant way of @@ -58,7 +61,7 @@ async function renderButton({ buttonText }: RenderInputs) { return { ...renderOutput, button: screen.getByRole('button', { name: new RegExp(buttonText) }), - unlinkCoderAccount: ejectToken, + unlinkCoderAccount: unlinkToken, refreshWorkspaces: refetch, }; } diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx index 3d9dbcf6..a6ccfb19 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx @@ -102,7 +102,7 @@ export const ExtraActionsButton = ({ const hookId = useId(); const [loadedAnchor, setLoadedAnchor] = useState(); const refreshWorkspaces = useRefreshWorkspaces(); - const { ejectToken } = useInternalCoderAuth(); + const { unlinkToken } = useInternalCoderAuth(); const styles = useStyles(); const closeMenu = () => setLoadedAnchor(undefined); @@ -178,7 +178,7 @@ export const ExtraActionsButton = ({ { - ejectToken(); + unlinkToken(); closeMenu(); }} > diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts index 5f82e6b7..305a5bab 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { workspaces, workspacesByRepo } from '../../api/queryOptions'; import type { CoderWorkspacesConfig } from '../../hooks/useCoderWorkspacesConfig'; -import { useCoderSdk } from '../../hooks/useCoderSdk'; +import { useCoderApi } from '../../hooks/useCoderApi'; import { useInternalCoderAuth } from '../../components/CoderProvider'; type QueryInput = Readonly<{ @@ -13,13 +13,13 @@ export function useCoderWorkspacesQuery({ coderQuery, workspacesConfig, }: QueryInput) { - const sdk = useCoderSdk(); + const api = useCoderApi(); const auth = useInternalCoderAuth(); const hasRepoData = workspacesConfig && workspacesConfig.repoUrl; const queryOptions = hasRepoData - ? workspacesByRepo({ auth, sdk, coderQuery, workspacesConfig }) - : workspaces({ auth, sdk, coderQuery }); + ? workspacesByRepo({ auth, api, coderQuery, workspacesConfig }) + : workspaces({ auth, api, coderQuery }); return useQuery(queryOptions); } diff --git a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx index 83309a08..65029704 100644 --- a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx +++ b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx @@ -25,7 +25,7 @@ import { getMockQueryClient, } from '../testHelpers/setup'; import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; -import { CODER_QUERY_KEY_PREFIX } from '../api/queryOptions'; +import { CODER_QUERY_KEY_PREFIX } from '../plugin'; import { mockWorkspacesList } from '../testHelpers/mockCoderPluginData'; type RenderUseQueryOptions< @@ -52,11 +52,11 @@ async function renderCoderQuery< } = options; let latestRegisterNewToken!: CoderAuth['registerNewToken']; - let latestEjectToken!: CoderAuth['ejectToken']; + let latestUnlinkToken!: CoderAuth['unlinkToken']; const AuthEscapeHatch = () => { const auth = useEndUserCoderAuth(); latestRegisterNewToken = auth.registerNewToken; - latestEjectToken = auth.ejectToken; + latestUnlinkToken = auth.unlinkToken; return null; }; @@ -91,15 +91,15 @@ async function renderCoderQuery< return act(() => latestRegisterNewToken(mockCoderAuthToken)); }; - const ejectToken = () => { - return act(() => latestEjectToken()); + const unlinkToken = () => { + return act(() => latestUnlinkToken()); }; if (authenticateOnMount) { registerMockToken(); } - return { ...renderOutput, registerMockToken, ejectToken }; + return { ...renderOutput, registerMockToken, unlinkToken }; } describe(`${useCoderQuery.name}`, () => { @@ -113,14 +113,17 @@ describe(`${useCoderQuery.name}`, () => { */ describe('Hook functionality', () => { it('Disables requests while user is not authenticated', async () => { - const { result, registerMockToken, ejectToken } = await renderCoderQuery({ - authenticateOnMount: false, - queryOptions: { - queryKey: ['workspaces'], - queryFn: ({ sdk }) => sdk.getWorkspaces({ q: 'owner:me' }), - select: response => response.workspaces, + const { result, registerMockToken, unlinkToken } = await renderCoderQuery( + { + authenticateOnMount: false, + queryOptions: { + queryKey: ['workspaces'], + queryFn: ({ coderApi: api }) => + api.getWorkspaces({ q: 'owner:me' }), + select: response => response.workspaces, + }, }, - }); + ); expect(result.current.isLoading).toBe(true); registerMockToken(); @@ -131,7 +134,7 @@ describe(`${useCoderQuery.name}`, () => { expect(result.current.data?.length).toBeGreaterThan(0); }); - ejectToken(); + unlinkToken(); await waitFor(() => expect(result.current.isLoading).toBe(true)); }); @@ -181,7 +184,7 @@ describe(`${useCoderQuery.name}`, () => { }); it('Disables everything when the user unlinks their access token', async () => { - const { result, ejectToken } = await renderCoderQuery({ + const { result, unlinkToken } = await renderCoderQuery({ queryOptions: { queryKey: ['workspaces'], queryFn: () => Promise.resolve(mockWorkspacesList), @@ -198,7 +201,7 @@ describe(`${useCoderQuery.name}`, () => { ); }); - ejectToken(); + unlinkToken(); await waitFor(() => { expect(result.current).toEqual( @@ -226,7 +229,7 @@ describe(`${useCoderQuery.name}`, () => { const { promise, reject } = createInvertedPromise(); const queryFn = jest.fn(() => promise); - const { ejectToken } = await renderCoderQuery({ + const { unlinkToken } = await renderCoderQuery({ queryOptions: { queryFn, queryKey: ['blah'], @@ -238,7 +241,7 @@ describe(`${useCoderQuery.name}`, () => { }); await waitFor(() => expect(queryFn).toHaveBeenCalled()); - ejectToken(); + unlinkToken(); queryFn.mockRestore(); act(() => reject(new Error("Don't feel like giving you data today"))); diff --git a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts index 6dff0240..95dcdffd 100644 --- a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts +++ b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts @@ -1,6 +1,6 @@ /** * @file Defines a couple of wrappers over React Query/Tanstack Query that make - * it easier to use the Coder SDK within UI logic. + * it easier to use the Coder API within UI logic. * * These hooks are designed 100% for end-users, and should not be used * internally. Use useEndUserCoderAuth when working with auth logic within these @@ -25,12 +25,12 @@ import { import { DEFAULT_TANSTACK_QUERY_RETRY_COUNT } from '../typesConstants'; import { useEndUserCoderAuth } from '../components/CoderProvider'; import { CODER_QUERY_KEY_PREFIX } from '../api/queryOptions'; -import { useCoderSdk } from './useCoderSdk'; -import type { BackstageCoderSdk } from '../api/CoderClient'; +import { useCoderApi } from './useCoderApi'; +import type { BackstageCoderApi } from '../api/CoderClient'; export type CoderQueryFunctionContext = QueryFunctionContext & { - sdk: BackstageCoderSdk; + coderApi: BackstageCoderApi; }; export type CoderQueryFunction< @@ -63,7 +63,7 @@ export function useCoderQuery< ): UseQueryResult { const queryClient = useQueryClient(); const { isAuthenticated } = useEndUserCoderAuth(); - const sdk = useCoderSdk(); + const coderApi = useCoderApi(); let patchedQueryKey = queryOptions.queryKey; if ( @@ -98,7 +98,7 @@ export function useCoderQuery< throw new Error('Cannot complete request - user is not authenticated'); } - return queryOptions.queryFn({ ...context, sdk }); + return queryOptions.queryFn({ ...context, coderApi }); }, refetchInterval: (data, query) => { diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderApi.ts similarity index 51% rename from plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts rename to plugins/backstage-plugin-coder/src/hooks/useCoderApi.ts index 7b7017a1..962f009c 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderApi.ts @@ -1,13 +1,16 @@ /** - * @file This defines the general helper for accessing the Coder SDK from + * @file This defines the general helper for accessing the Coder API from * Backstage in a type-safe way. * * This hook is meant to be used both internally AND externally. */ import { useApi } from '@backstage/core-plugin-api'; -import { coderClientApiRef, type BackstageCoderSdk } from '../api/CoderClient'; +import { + type BackstageCoderApi, + coderClientWrapperApiRef, +} from '../api/CoderClient'; -export function useCoderSdk(): BackstageCoderSdk { - const { sdk } = useApi(coderClientApiRef); - return sdk; +export function useCoderApi(): BackstageCoderApi { + const { api } = useApi(coderClientWrapperApiRef); + return api; } diff --git a/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx b/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx index 90cac33d..2662b1e6 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx +++ b/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx @@ -8,11 +8,11 @@ import { mockBackstageAssetsEndpoint, mockBackstageUrlRoot, getMockConfigApi, - mockBackstageApiEndpointWithoutSdkPath, + mockBackstageApiEndpointWithoutVersionSuffix, } from '../testHelpers/mockBackstageData'; function renderUseUrlSync() { - let proxyEndpoint: string = mockBackstageApiEndpointWithoutSdkPath; + let proxyEndpoint: string = mockBackstageApiEndpointWithoutVersionSuffix; const mockDiscoveryApi: DiscoveryApi = { getBaseUrl: async () => proxyEndpoint, }; @@ -53,7 +53,7 @@ describe(`${useUrlSync.name}`, () => { state: { baseUrl: mockBackstageUrlRoot, assetsRoute: mockBackstageAssetsEndpoint, - apiRoute: mockBackstageApiEndpointWithoutSdkPath, + apiRoute: mockBackstageApiEndpointWithoutVersionSuffix, }, }), ); diff --git a/plugins/backstage-plugin-coder/src/plugin.ts b/plugins/backstage-plugin-coder/src/plugin.ts index 904b7705..d165c36f 100644 --- a/plugins/backstage-plugin-coder/src/plugin.ts +++ b/plugins/backstage-plugin-coder/src/plugin.ts @@ -8,7 +8,10 @@ import { } from '@backstage/core-plugin-api'; import { rootRouteRef } from './routes'; import { UrlSync, urlSyncApiRef } from './api/UrlSync'; -import { CoderClient, coderClientApiRef } from './api/CoderClient'; +import { + CoderClientWrapper, + coderClientWrapperApiRef, +} from './api/CoderClient'; export const coderPlugin = createPlugin({ id: 'coder', @@ -27,13 +30,13 @@ export const coderPlugin = createPlugin({ }, }), createApiFactory({ - api: coderClientApiRef, + api: coderClientWrapperApiRef, deps: { urlSync: urlSyncApiRef, identityApi: identityApiRef, }, factory: ({ urlSync, identityApi }) => { - return new CoderClient({ + return new CoderClientWrapper({ apis: { urlSync, identityApi }, }); }, @@ -190,10 +193,13 @@ export { useWorkspacesCardContext } from './components/CoderWorkspacesCard/Root' * General custom hooks that can be used in various places. */ export { useCoderWorkspacesConfig } from './hooks/useCoderWorkspacesConfig'; -export { useCoderSdk } from './hooks/useCoderSdk'; -export { useEndUserCoderAuth as useCoderAuth } from './components/CoderProvider/CoderAuthProvider'; +export { useCoderApi } from './hooks/useCoderApi'; export { useCoderQuery } from './hooks/reactQueryWrappers'; +// Deliberately renamed so that end users don't have to be aware that there are +// two different versions of the auth hook +export { useEndUserCoderAuth as useCoderAuth } from './components/CoderProvider/CoderAuthProvider'; + /** * General constants */ @@ -203,3 +209,4 @@ export { CODER_QUERY_KEY_PREFIX } from './api/queryOptions'; * All custom types */ export type { CoderAppConfig } from './components/CoderProvider'; +export type * from './api/vendoredSdk/api/typesGenerated'; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts index 8c96f8d2..843e4743 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts @@ -33,7 +33,10 @@ import { defaultUrlPrefixes, urlSyncApiRef, } from '../api/UrlSync'; -import { CoderClient, coderClientApiRef } from '../api/CoderClient'; +import { + CoderClientWrapper, + coderClientWrapperApiRef, +} from '../api/CoderClient'; /** * This is the key that Backstage checks from the entity data to determine the @@ -68,24 +71,24 @@ export const mockBackstageUrlRoot = 'http://localhost:7007'; /** * A version of the mock API endpoint that doesn't have the Coder API versioning - * prefix. Mainly used for tests that need to assert that the core API URL is - * formatted correctly, before the CoderSdk adds anything else to the end + * suffix. Mainly used for tests that need to assert that the core API URL is + * formatted correctly, before the Coder API adds anything else to the end * * The string literal expression is complicated, but hover over it to see what * the final result is. */ -export const mockBackstageApiEndpointWithoutSdkPath = +export const mockBackstageApiEndpointWithoutVersionSuffix = `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}` as const; /** * The API endpoint to use with the mock server during testing. Adds additional - * path information that will normally be added via the Coder SDK. + * path information that will normally be added via the Coder API. * * The string literal expression is complicated, but hover over it to see what * the final result is. */ export const mockBackstageApiEndpoint = - `${mockBackstageApiEndpointWithoutSdkPath}/api/v2` as const; + `${mockBackstageApiEndpointWithoutVersionSuffix}/api/v2` as const; /** * The assets endpoint to use during testing. @@ -173,7 +176,7 @@ const authedState = { error: undefined, isAuthenticated: true, registerNewToken: jest.fn(), - ejectToken: jest.fn(), + unlinkToken: jest.fn(), } as const satisfies Partial; const notAuthedState = { @@ -181,7 +184,7 @@ const notAuthedState = { error: undefined, isAuthenticated: false, registerNewToken: jest.fn(), - ejectToken: jest.fn(), + unlinkToken: jest.fn(), } as const satisfies Partial; export const mockAuthStates = { @@ -309,7 +312,7 @@ export function getMockApiList(): readonly ApiTuple[] { }, }); - const mockCoderClient = new CoderClient({ + const mockCoderClient = new CoderClientWrapper({ initialToken: mockCoderAuthToken, apis: { urlSync: mockUrlSyncApi, @@ -327,6 +330,6 @@ export function getMockApiList(): readonly ApiTuple[] { // Custom APIs specific to the Coder plugin [urlSyncApiRef, mockUrlSyncApi], - [coderClientApiRef, mockCoderClient], + [coderClientWrapperApiRef, mockCoderClient], ]; } diff --git a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx index cc8c67ad..b7d3191a 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx +++ b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx @@ -10,7 +10,11 @@ import { /* eslint-enable @backstage/no-undeclared-imports */ import React from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + type QueryClientConfig, + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { type CoderAuth, @@ -93,13 +97,16 @@ export function suppressErrorBoundaryWarnings(): void { afterEachCleanupFunctions.push(() => augmentedConsoleError.mockClear()); } -export function getMockQueryClient(): QueryClient { +export function getMockQueryClient(config?: QueryClientConfig): QueryClient { return new QueryClient({ + ...(config ?? {}), defaultOptions: { + ...(config?.defaultOptions ?? {}), queries: { retry: false, refetchOnWindowFocus: false, networkMode: 'offlineFirst', + ...(config?.defaultOptions?.queries ?? {}), }, }, });