diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts index 9addcd1a..2bfa6b24 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts @@ -1,8 +1,4 @@ -import { - CODER_AUTH_HEADER_KEY, - CoderClient, - disabledClientError, -} from './CoderClient'; +import { CODER_AUTH_HEADER_KEY, CoderClient } from './CoderClient'; import type { IdentityApi } from '@backstage/core-plugin-api'; import { UrlSync } from './UrlSync'; import { rest } from 'msw'; @@ -12,8 +8,8 @@ import { delay } from '../utils/time'; import { mockWorkspacesList, mockWorkspacesListForRepoSearch, -} from '../testHelpers/mockCoderAppData'; -import type { Workspace, WorkspacesResponse } from '../typesConstants'; +} from '../testHelpers/mockCoderPluginData'; +import type { Workspace, WorkspacesResponse } from './vendoredSdk'; import { getMockConfigApi, getMockDiscoveryApi, @@ -100,50 +96,6 @@ describe(`${CoderClient.name}`, () => { }); }); - describe('cleanupClient functionality', () => { - it('Will prevent any new SDK requests from going through', async () => { - const client = new CoderClient({ apis: getConstructorApis() }); - client.cleanupClient(); - - // Request should fail, even though token is valid - await expect(() => { - return client.syncToken(mockCoderAuthToken); - }).rejects.toThrow(disabledClientError); - - await expect(() => { - return client.sdk.getWorkspaces({ - q: 'owner:me', - limit: 0, - }); - }).rejects.toThrow(disabledClientError); - }); - - it('Will abort any pending requests', async () => { - const client = new CoderClient({ - initialToken: mockCoderAuthToken, - apis: getConstructorApis(), - }); - - // Sanity check to ensure that request can still go through normally - const workspacesPromise1 = client.sdk.getWorkspaces({ - q: 'owner:me', - limit: 0, - }); - - await expect(workspacesPromise1).resolves.toEqual({ - workspaces: mockWorkspacesList, - count: mockWorkspacesList.length, - }); - - const workspacesPromise2 = client.sdk.getWorkspaces({ - q: 'owner:me', - limit: 0, - }); - client.cleanupClient(); - await expect(() => workspacesPromise2).rejects.toThrow(); - }); - }); - // Eventually the Coder SDK is going to get too big to test every single // function. Focus tests on the functionality specifically being patched in // for Backstage @@ -180,10 +132,10 @@ describe(`${CoderClient.name}`, () => { }); const { urlSync } = apis; - const apiEndpoint = await urlSync.getApiEndpoint(); + const assetsEndpoint = await urlSync.getAssetsEndpoint(); - const allWorkspacesAreRemapped = !workspaces.some(ws => - ws.template_icon.startsWith(apiEndpoint), + const allWorkspacesAreRemapped = workspaces.every(ws => + ws.template_icon.startsWith(assetsEndpoint), ); expect(allWorkspacesAreRemapped).toBe(true); diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.ts index 7c09f72c..4c5333dd 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.ts @@ -1,19 +1,19 @@ -import globalAxios, { +import { AxiosError, - type AxiosInstance, type InternalAxiosRequestConfig as RequestConfig, } from 'axios'; import { type IdentityApi, createApiRef } from '@backstage/core-plugin-api'; -import { - type Workspace, - CODER_API_REF_ID_PREFIX, - WorkspacesRequest, - WorkspacesResponse, - User, -} from '../typesConstants'; +import { CODER_API_REF_ID_PREFIX } from '../typesConstants'; import type { UrlSync } from './UrlSync'; import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; -import { CoderSdk } from './MockCoderSdk'; +import { + type CoderSdk, + type User, + type Workspace, + type WorkspacesRequest, + type WorkspacesResponse, + makeCoderSdk, +} from './vendoredSdk'; export const CODER_AUTH_HEADER_KEY = 'Coder-Session-Token'; const DEFAULT_REQUEST_TIMEOUT_MS = 20_000; @@ -39,11 +39,6 @@ type CoderClientApi = Readonly<{ * Return value indicates whether the token is valid. */ syncToken: (newToken: string) => Promise; - - /** - * Cleans up a client instance, removing its links to all external systems. - */ - cleanupClient: () => void; }>; const sharedCleanupAbortReason = new DOMException( @@ -59,19 +54,30 @@ export const disabledClientError = new Error( ); type ConstructorInputs = Readonly<{ + /** + * initialToken is strictly for testing, and is basically limited to making it + * easier to test API logic. + * + * If trying to test UI logic that depends on CoderClient, it's probably + * better to interact with CoderClient indirectly through the auth components, + * so that React state is aware of everything. + */ initialToken?: string; - requestTimeoutMs?: number; + requestTimeoutMs?: number; apis: Readonly<{ urlSync: UrlSync; identityApi: IdentityApi; }>; }>; +type RequestInterceptor = ( + config: RequestConfig, +) => RequestConfig | Promise; + export class CoderClient implements CoderClientApi { private readonly urlSync: UrlSync; private readonly identityApi: IdentityApi; - private readonly axios: AxiosInstance; private readonly requestTimeoutMs: number; private readonly cleanupController: AbortController; @@ -82,33 +88,28 @@ export class CoderClient implements CoderClientApi { constructor(inputs: ConstructorInputs) { const { - apis, initialToken, + apis: { urlSync, identityApi }, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, } = inputs; - const { urlSync, identityApi } = apis; this.urlSync = urlSync; this.identityApi = identityApi; - this.axios = globalAxios.create(); - this.loadedSessionToken = initialToken; this.requestTimeoutMs = requestTimeoutMs; - this.cleanupController = new AbortController(); this.trackedEjectionIds = new Set(); - this.sdk = this.getBackstageCoderSdk(this.axios); + this.sdk = this.createBackstageCoderSdk(); this.addBaseRequestInterceptors(); } private addRequestInterceptor( - requestInterceptor: ( - config: RequestConfig, - ) => RequestConfig | Promise, + requestInterceptor: RequestInterceptor, errorInterceptor?: (error: unknown) => unknown, ): number { - const ejectionId = this.axios.interceptors.request.use( + const axios = this.sdk.getAxiosInstance(); + const ejectionId = axios.interceptors.request.use( requestInterceptor, errorInterceptor, ); @@ -120,7 +121,8 @@ 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 - this.axios.interceptors.request.eject(ejectionId); + const axios = this.sdk.getAxiosInstance(); + axios.interceptors.request.eject(ejectionId); if (!this.trackedEjectionIds.has(ejectionId)) { return false; @@ -179,10 +181,8 @@ export class CoderClient implements CoderClientApi { this.addRequestInterceptor(baseRequestInterceptor, baseErrorInterceptor); } - private getBackstageCoderSdk( - axiosInstance: AxiosInstance, - ): BackstageCoderSdk { - const baseSdk = new CoderSdk(axiosInstance); + private createBackstageCoderSdk(): BackstageCoderSdk { + const baseSdk = makeCoderSdk(); const getWorkspaces: (typeof baseSdk)['getWorkspaces'] = async request => { const workspacesRes = await baseSdk.getWorkspaces(request); @@ -335,23 +335,6 @@ export class CoderClient implements CoderClientApi { this.removeRequestInterceptorById(validationId); } }; - - cleanupClient = (): void => { - this.trackedEjectionIds.forEach(id => { - this.axios.interceptors.request.eject(id); - }); - - this.trackedEjectionIds.clear(); - this.cleanupController.abort(sharedCleanupAbortReason); - this.loadedSessionToken = undefined; - - // Not using this.addRequestInterceptor, because we don't want to track this - // interceptor at all. It should never be ejected once the client has been - // disabled - this.axios.interceptors.request.use(() => { - throw disabledClientError; - }); - }; } function appendParamToQuery( diff --git a/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts b/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts deleted file mode 100644 index 3100242b..00000000 --- a/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @file This is a temporary (and significantly limited) implementation of the - * "Coder SDK" that will eventually be imported from Coder core - * - * @todo Replace this with a full, proper implementation, and then expose it to - * plugin users. - */ -import globalAxios, { type AxiosInstance } from 'axios'; -import { - type User, - type WorkspacesRequest, - type WorkspacesResponse, -} from '../typesConstants'; - -type CoderSdkApi = { - getAuthenticatedUser: () => Promise; - getWorkspaces: (request: WorkspacesRequest) => Promise; -}; - -export class CoderSdk implements CoderSdkApi { - private readonly axios: AxiosInstance; - - constructor(axiosInstance?: AxiosInstance) { - this.axios = axiosInstance ?? globalAxios.create(); - } - - getWorkspaces = async ( - request: WorkspacesRequest, - ): Promise => { - const urlParams = new URLSearchParams({ - q: request.q ?? '', - limit: String(request.limit || 0), - after_id: request.after_id ?? '', - offset: String(request.offset || 0), - }); - - const response = await this.axios.get( - `/workspaces?${urlParams.toString()}`, - ); - - return response.data; - }; - - getAuthenticatedUser = async (): Promise => { - const response = await this.axios.get('/users/me'); - return response.data; - }; -} diff --git a/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts b/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts index 4932edea..62001e4e 100644 --- a/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts +++ b/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts @@ -4,8 +4,8 @@ import { getMockConfigApi, getMockDiscoveryApi, mockBackstageAssetsEndpoint, - mockBackstageApiEndpoint, mockBackstageUrlRoot, + mockBackstageApiEndpointWithoutSdkPath, } 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: mockBackstageApiEndpoint, + apiRoute: mockBackstageApiEndpointWithoutSdkPath, assetsRoute: mockBackstageAssetsEndpoint, }); }); @@ -50,7 +50,7 @@ describe(`${UrlSync.name}`, () => { expect(newSnapshot).toEqual({ baseUrl: 'blah', - apiRoute: 'blah/coder/api/v2', + apiRoute: 'blah/coder', assetsRoute: 'blah/coder', }); }); @@ -76,7 +76,7 @@ describe(`${UrlSync.name}`, () => { expect(onChange).toHaveBeenCalledWith({ baseUrl: 'blah', - apiRoute: 'blah/coder/api/v2', + apiRoute: 'blah/coder', assetsRoute: 'blah/coder', } satisfies UrlSyncSnapshot); diff --git a/plugins/backstage-plugin-coder/src/api/UrlSync.ts b/plugins/backstage-plugin-coder/src/api/UrlSync.ts index ae05294b..8b3548d6 100644 --- a/plugins/backstage-plugin-coder/src/api/UrlSync.ts +++ b/plugins/backstage-plugin-coder/src/api/UrlSync.ts @@ -42,14 +42,10 @@ const PROXY_URL_KEY_FOR_DISCOVERY_API = 'proxy'; type UrlPrefixes = Readonly<{ proxyPrefix: string; - apiRoutePrefix: string; - assetsRoutePrefix: string; }>; export const defaultUrlPrefixes = { proxyPrefix: `/api/proxy`, - apiRoutePrefix: '/api/v2', - assetsRoutePrefix: '', // Deliberately left as empty string } as const satisfies UrlPrefixes; export type UrlSyncSnapshot = Readonly<{ @@ -104,12 +100,10 @@ export class UrlSync implements UrlSyncApi { } private prepareNewSnapshot(newProxyUrl: string): UrlSyncSnapshot { - const { assetsRoutePrefix, apiRoutePrefix } = this.urlPrefixes; - return { baseUrl: newProxyUrl.replace(proxyRouteReplacer, ''), - assetsRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}${assetsRoutePrefix}`, - apiRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}${apiRoutePrefix}`, + assetsRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}`, + apiRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}`, }; } diff --git a/plugins/backstage-plugin-coder/src/api/queryOptions.ts b/plugins/backstage-plugin-coder/src/api/queryOptions.ts index b10ecfe2..4e55861d 100644 --- a/plugins/backstage-plugin-coder/src/api/queryOptions.ts +++ b/plugins/backstage-plugin-coder/src/api/queryOptions.ts @@ -1,5 +1,5 @@ import type { UseQueryOptions } from '@tanstack/react-query'; -import type { Workspace, WorkspacesRequest } from '../typesConstants'; +import type { Workspace, WorkspacesRequest } from './vendoredSdk'; import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; import type { BackstageCoderSdk } from './CoderClient'; import type { CoderAuth } from '../components/CoderProvider'; @@ -44,13 +44,13 @@ function getSharedWorkspacesQueryKey(coderQuery: string) { type WorkspacesFetchInputs = Readonly<{ auth: CoderAuth; - coderSdk: BackstageCoderSdk; + sdk: BackstageCoderSdk; coderQuery: string; }>; export function workspaces({ auth, - coderSdk, + sdk, coderQuery, }: WorkspacesFetchInputs): UseQueryOptions { const enabled = auth.isAuthenticated; @@ -61,7 +61,7 @@ export function workspaces({ keepPreviousData: enabled && coderQuery !== '', refetchInterval: getCoderWorkspacesRefetchInterval, queryFn: async () => { - const res = await coderSdk.getWorkspaces({ + const res = await sdk.getWorkspaces({ q: coderQuery, limit: 0, }); @@ -79,7 +79,7 @@ type WorkspacesByRepoFetchInputs = Readonly< export function workspacesByRepo({ coderQuery, - coderSdk, + sdk, auth, workspacesConfig, }: WorkspacesByRepoFetchInputs): UseQueryOptions { @@ -95,7 +95,7 @@ export function workspacesByRepo({ refetchInterval: getCoderWorkspacesRefetchInterval, queryFn: async () => { const request: WorkspacesRequest = { q: coderQuery, limit: 0 }; - const res = await coderSdk.getWorkspacesByRepo(request, workspacesConfig); + const res = await sdk.getWorkspacesByRepo(request, workspacesConfig); return res.workspaces; }, }; diff --git a/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx b/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx index c1f2bc61..5843a180 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx @@ -39,7 +39,7 @@ class ErrorBoundaryCore extends Component< render() { const { children, fallbackUi } = this.props; - return this.state.hasError ? fallbackUi : children; + return <>{this.state.hasError ? fallbackUi : children}; } } diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index c9b6fbb1..664bb311 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -165,19 +165,23 @@ function useAuthState(): CoderAuth { return unsubscribe; }, [queryClient]); + const registerNewToken = useCallback((newToken: string) => { + if (newToken !== '') { + setAuthToken(newToken); + } + }, []); + + const ejectToken = useCallback(() => { + setAuthToken(''); + window.localStorage.removeItem(TOKEN_STORAGE_KEY); + queryClient.removeQueries({ queryKey: [CODER_QUERY_KEY_PREFIX] }); + }, [queryClient]); + return { ...authState, isAuthenticated: validAuthStatuses.includes(authState.status), - registerNewToken: newToken => { - if (newToken !== '') { - setAuthToken(newToken); - } - }, - ejectToken: () => { - setAuthToken(''); - window.localStorage.removeItem(TOKEN_STORAGE_KEY); - queryClient.removeQueries({ queryKey: [CODER_QUERY_KEY_PREFIX] }); - }, + registerNewToken, + ejectToken, }; } diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx index a8cbef6c..8acc04a1 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx @@ -10,7 +10,7 @@ import { mockWorkspaceNoParameters, mockWorkspaceWithMatch2, mockWorkspacesList, -} from '../../testHelpers/mockCoderAppData'; +} from '../../testHelpers/mockCoderPluginData'; import { type CoderAuthStatus } from '../CoderProvider'; import { CoderWorkspacesCard } from './CoderWorkspacesCard'; import userEvent from '@testing-library/user-event'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx index 0ae1d918..5be7284b 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderInCoderEnvironment } from '../../testHelpers/setup'; -import type { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData'; import { type WorkspacesCardContext, diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx index 0866d95a..452f0a9c 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx @@ -15,7 +15,7 @@ import { useCoderWorkspacesConfig, type CoderWorkspacesConfig, } from '../../hooks/useCoderWorkspacesConfig'; -import type { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { useCoderWorkspacesQuery } from '../../hooks/useCoderWorkspacesQuery'; import { CoderAuthFormCardWrapper } from '../CoderAuthFormCardWrapper'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx index 50bc1de1..bc7e0273 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx @@ -3,8 +3,8 @@ import { type WorkspacesListProps, WorkspacesList } from './WorkspacesList'; import { renderInCoderEnvironment } from '../../testHelpers/setup'; import { CardContext, WorkspacesCardContext, WorkspacesQuery } from './Root'; import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData'; -import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData'; -import { Workspace } from '../../typesConstants'; +import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderPluginData'; +import type { Workspace } from '../../api/vendoredSdk'; import { screen } from '@testing-library/react'; type RenderInputs = Readonly<{ diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx index 1e47b08a..9301d6a4 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx @@ -1,7 +1,7 @@ import React, { type HTMLAttributes, type ReactNode, Fragment } from 'react'; import { type Theme, makeStyles } from '@material-ui/core'; -import type { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { useWorkspacesCardContext } from './Root'; import { WorkspacesListItem } from './WorkspacesListItem'; import { Placeholder } from './Placeholder'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx index 03ff2623..471d3356 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx @@ -1,9 +1,13 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { renderInCoderEnvironment } from '../../testHelpers/setup'; -import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData'; -import type { Workspace } from '../../typesConstants'; +import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderPluginData'; +import type { Workspace } from '../../api/vendoredSdk'; import { WorkspacesListItem } from './WorkspacesListItem'; +import { + MockWorkspaceAgent, + MockWorkspaceResource, +} from '../../testHelpers/coderEntities'; type RenderInput = Readonly<{ isOnline?: boolean; @@ -19,9 +23,11 @@ async function renderListItem(inputs?: RenderInput) { status: isOnline ? 'running' : 'stopped', resources: [ { + ...MockWorkspaceResource, id: '1', agents: [ { + ...MockWorkspaceAgent, id: '2', status: isOnline ? 'connected' : 'disconnected', }, diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx index f7292e51..a5a588ae 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx @@ -11,7 +11,8 @@ import { useId } from '../../hooks/hookPolyfills'; import { useCoderAppConfig } from '../CoderProvider'; import { getWorkspaceAgentStatuses } from '../../utils/workspaces'; -import type { Workspace, WorkspaceStatus } from '../../typesConstants'; +import type { WorkspaceStatus } from '../../api/vendoredSdk'; +import type { Workspace } from '../../api/vendoredSdk'; import { WorkspacesListIcon } from './WorkspacesListIcon'; import { VisuallyHidden } from '../VisuallyHidden'; diff --git a/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts b/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts index 3b777c5e..ce15f948 100644 --- a/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts +++ b/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts @@ -25,5 +25,11 @@ function useIdPolyfill(): string { return readonlyId; } +const ReactWithNewerHooks = React as typeof React & { + useId?: () => string; +}; + export const useId = - typeof React.useId === 'undefined' ? useIdPolyfill : React.useId; + typeof ReactWithNewerHooks.useId === 'undefined' + ? useIdPolyfill + : ReactWithNewerHooks.useId; diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts index 8fbec12c..7b7017a1 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts @@ -1,7 +1,13 @@ +/** + * @file This defines the general helper for accessing the Coder SDK 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'; export function useCoderSdk(): BackstageCoderSdk { - const coderClient = useApi(coderClientApiRef); - return coderClient.sdk; + const { sdk } = useApi(coderClientApiRef); + return sdk; } diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts index d29e64a5..49535619 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts @@ -6,7 +6,7 @@ import { mockCoderWorkspacesConfig } from '../testHelpers/mockBackstageData'; import { mockWorkspaceNoParameters, mockWorkspacesList, -} from '../testHelpers/mockCoderAppData'; +} from '../testHelpers/mockCoderPluginData'; beforeAll(() => { jest.useFakeTimers(); diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts index 4e41ef86..63b4f2f7 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts @@ -13,13 +13,13 @@ export function useCoderWorkspacesQuery({ coderQuery, workspacesConfig, }: QueryInput) { + const sdk = useCoderSdk(); const auth = useInternalCoderAuth(); - const coderSdk = useCoderSdk(); const hasRepoData = workspacesConfig && workspacesConfig.repoUrl; const queryOptions = hasRepoData - ? workspacesByRepo({ auth, coderSdk, coderQuery, workspacesConfig }) - : workspaces({ auth, coderSdk, coderQuery }); + ? workspacesByRepo({ auth, sdk, coderQuery, workspacesConfig }) + : workspaces({ auth, sdk, coderQuery }); return useQuery(queryOptions); } diff --git a/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx b/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx index 164242f7..90cac33d 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx +++ b/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx @@ -6,13 +6,13 @@ import { type UseUrlSyncResult, useUrlSync } from './useUrlSync'; import type { DiscoveryApi } from '@backstage/core-plugin-api'; import { mockBackstageAssetsEndpoint, - mockBackstageApiEndpoint, mockBackstageUrlRoot, getMockConfigApi, + mockBackstageApiEndpointWithoutSdkPath, } from '../testHelpers/mockBackstageData'; function renderUseUrlSync() { - let proxyEndpoint: string = mockBackstageApiEndpoint; + let proxyEndpoint: string = mockBackstageApiEndpointWithoutSdkPath; const mockDiscoveryApi: DiscoveryApi = { getBaseUrl: async () => proxyEndpoint, }; @@ -53,7 +53,7 @@ describe(`${useUrlSync.name}`, () => { state: { baseUrl: mockBackstageUrlRoot, assetsRoute: mockBackstageAssetsEndpoint, - apiRoute: mockBackstageApiEndpoint, + apiRoute: mockBackstageApiEndpointWithoutSdkPath, }, }), ); diff --git a/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts b/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts new file mode 100644 index 00000000..b5cf5abf --- /dev/null +++ b/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts @@ -0,0 +1,305 @@ +/** + * @file This is a subset of the mock data from the Coder OSS repo. No values + * are modified; if any values should be patched for Backstage testing, those + * should be updated in the mockCoderPluginData.ts file. + * + * @see {@link https://github.com/coder/coder/blob/main/site/src/testHelpers/entities.ts} + */ +import type * as TypesGen from '../api/vendoredSdk'; + +const MockOrganization: TypesGen.Organization = { + id: 'fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0', + name: 'Test Organization', + created_at: '', + updated_at: '', + is_default: true, +}; + +const MockOwnerRole: TypesGen.Role = { + name: 'owner', + display_name: 'Owner', + site_permissions: [], + organization_permissions: {}, + user_permissions: [], + organization_id: '', +}; + +export const MockUser: TypesGen.User = { + id: 'test-user', + username: 'TestUser', + email: 'test@coder.com', + created_at: '', + status: 'active', + organization_ids: [MockOrganization.id], + roles: [MockOwnerRole], + avatar_url: 'https://avatars.githubusercontent.com/u/95932066?s=200&v=4', + last_seen_at: '', + login_type: 'password', + theme_preference: '', + name: '', +}; + +const MockProvisionerJob: TypesGen.ProvisionerJob = { + created_at: '', + id: 'test-provisioner-job', + status: 'succeeded', + file_id: MockOrganization.id, + completed_at: '2022-05-17T17:39:01.382927298Z', + tags: { + scope: 'organization', + owner: '', + wowzers: 'whatatag', + isCapable: 'false', + department: 'engineering', + dreaming: 'true', + }, + queue_position: 0, + queue_size: 0, +}; + +const MockProvisioner: TypesGen.ProvisionerDaemon = { + created_at: '2022-05-17T17:39:01.382927298Z', + id: 'test-provisioner', + name: 'Test Provisioner', + provisioners: ['echo'], + tags: { scope: 'organization' }, + version: 'v2.34.5', + api_version: '1.0', +}; + +const MockTemplateVersion: TypesGen.TemplateVersion = { + id: 'test-template-version', + created_at: '2022-05-17T17:39:01.382927298Z', + updated_at: '2022-05-17T17:39:01.382927298Z', + template_id: 'test-template', + job: MockProvisionerJob, + name: 'test-version', + message: 'first version', + readme: `--- +name:Template test +--- +## Instructions +You can add instructions here + +[Some link info](https://coder.com)`, + created_by: MockUser, + archived: false, +}; + +const MockWorkspaceAgentLogSource: TypesGen.WorkspaceAgentLogSource = { + created_at: '2023-05-04T11:30:41.402072Z', + id: 'dc790496-eaec-4f88-a53f-8ce1f61a1fff', + display_name: 'Startup Script', + icon: '', + workspace_agent_id: '', +}; + +const MockBuildInfo: TypesGen.BuildInfoResponse = { + agent_api_version: '1.0', + external_url: 'file:///mock-url', + version: 'v99.999.9999+c9cdf14', + dashboard_url: 'https:///mock-url', + workspace_proxy: false, + upgrade_message: 'My custom upgrade message', + deployment_id: '510d407f-e521-4180-b559-eab4a6d802b8', +}; + +const MockWorkspaceApp: TypesGen.WorkspaceApp = { + id: 'test-app', + slug: 'test-app', + display_name: 'Test App', + icon: '', + subdomain: false, + health: 'disabled', + external: false, + url: '', + sharing_level: 'owner', + healthcheck: { + url: '', + interval: 0, + threshold: 0, + }, +}; + +const MockWorkspaceAgentScript: TypesGen.WorkspaceAgentScript = { + log_source_id: MockWorkspaceAgentLogSource.id, + cron: '', + log_path: '', + run_on_start: true, + run_on_stop: false, + script: "echo 'hello world'", + start_blocks_login: false, + timeout: 0, +}; + +export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { + apps: [MockWorkspaceApp], + architecture: 'amd64', + created_at: '', + environment_variables: {}, + id: 'test-workspace-agent', + name: 'a-workspace-agent', + operating_system: 'linux', + resource_id: '', + status: 'connected', + updated_at: '', + version: MockBuildInfo.version, + api_version: '1.0', + latency: { + 'Coder Embedded DERP': { + latency_ms: 32.55, + preferred: true, + }, + }, + connection_timeout_seconds: 120, + troubleshooting_url: 'https://coder.com/troubleshoot', + lifecycle_state: 'starting', + logs_length: 0, + logs_overflowed: false, + log_sources: [MockWorkspaceAgentLogSource], + scripts: [MockWorkspaceAgentScript], + startup_script_behavior: 'non-blocking', + subsystems: ['envbox', 'exectrace'], + health: { + healthy: true, + }, + display_apps: [ + 'ssh_helper', + 'port_forwarding_helper', + 'vscode', + 'vscode_insiders', + 'web_terminal', + ], +}; + +export const MockWorkspaceResource: TypesGen.WorkspaceResource = { + id: 'test-workspace-resource', + name: 'a-workspace-resource', + agents: [MockWorkspaceAgent], + created_at: '', + job_id: '', + type: 'google_compute_disk', + workspace_transition: 'start', + hide: false, + icon: '', + metadata: [{ key: 'size', value: '32GB', sensitive: false }], + daily_cost: 10, +}; + +const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { + build_number: 1, + created_at: '2022-05-17T17:39:01.382927298Z', + id: '1', + initiator_id: MockUser.id, + initiator_name: MockUser.username, + job: MockProvisionerJob, + template_version_id: MockTemplateVersion.id, + template_version_name: MockTemplateVersion.name, + transition: 'start', + updated_at: '2022-05-17T17:39:01.382927298Z', + workspace_name: 'test-workspace', + workspace_owner_id: MockUser.id, + workspace_owner_name: MockUser.username, + workspace_owner_avatar_url: MockUser.avatar_url, + workspace_id: '759f1d46-3174-453d-aa60-980a9c1442f3', + deadline: '2022-05-17T23:39:00.00Z', + reason: 'initiator', + resources: [MockWorkspaceResource], + status: 'running', + daily_cost: 20, +}; + +const MockTemplate: TypesGen.Template = { + id: 'test-template', + created_at: '2022-05-17T17:39:01.382927298Z', + updated_at: '2022-05-18T17:39:01.382927298Z', + organization_id: MockOrganization.id, + name: 'test-template', + display_name: 'Test Template', + provisioner: MockProvisioner.provisioners[0], + active_version_id: MockTemplateVersion.id, + active_user_count: 1, + build_time_stats: { + start: { + P50: 1000, + P95: 1500, + }, + stop: { + P50: 1000, + P95: 1500, + }, + delete: { + P50: 1000, + P95: 1500, + }, + }, + description: 'This is a test description.', + default_ttl_ms: 24 * 60 * 60 * 1000, + activity_bump_ms: 1 * 60 * 60 * 1000, + autostop_requirement: { + days_of_week: ['sunday'], + weeks: 1, + }, + autostart_requirement: { + days_of_week: [ + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday', + ], + }, + created_by_id: 'test-creator-id', + created_by_name: 'test_creator', + icon: '/icon/code.svg', + allow_user_cancel_workspace_jobs: true, + failure_ttl_ms: 0, + time_til_dormant_ms: 0, + time_til_dormant_autodelete_ms: 0, + allow_user_autostart: true, + allow_user_autostop: true, + require_active_version: false, + deprecated: false, + deprecation_message: '', + max_port_share_level: 'public', +}; + +const MockWorkspaceAutostartEnabled: TypesGen.UpdateWorkspaceAutostartRequest = + { + // Runs at 9:30am Monday through Friday using Canada/Eastern + // (America/Toronto) time + schedule: 'CRON_TZ=Canada/Eastern 30 9 * * 1-5', + }; + +export const MockWorkspace: TypesGen.Workspace = { + id: 'test-workspace', + name: 'Test-Workspace', + created_at: '', + updated_at: '', + template_id: MockTemplate.id, + template_name: MockTemplate.name, + template_icon: MockTemplate.icon, + template_display_name: MockTemplate.display_name, + template_allow_user_cancel_workspace_jobs: + MockTemplate.allow_user_cancel_workspace_jobs, + template_active_version_id: MockTemplate.active_version_id, + template_require_active_version: MockTemplate.require_active_version, + outdated: false, + owner_id: MockUser.id, + organization_id: MockOrganization.id, + owner_name: MockUser.username, + owner_avatar_url: 'https://avatars.githubusercontent.com/u/7122116?v=4', + autostart_schedule: MockWorkspaceAutostartEnabled.schedule, + ttl_ms: 2 * 60 * 60 * 1000, + latest_build: MockWorkspaceBuild, + last_used_at: '2022-05-16T15:29:10.302441433Z', + health: { + healthy: true, + failing_agents: [], + }, + automatic_updates: 'never', + allow_renames: true, + favorite: false, +}; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts index 34f11218..8c96f8d2 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts @@ -67,13 +67,25 @@ export const rawRepoUrl = `${cleanedRepoUrl}/tree/main/`; export const mockBackstageUrlRoot = 'http://localhost:7007'; /** - * The API endpoint to use with the mock server during testing. + * 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 + * + * The string literal expression is complicated, but hover over it to see what + * the final result is. + */ +export const mockBackstageApiEndpointWithoutSdkPath = + `${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. * * The string literal expression is complicated, but hover over it to see what * the final result is. */ export const mockBackstageApiEndpoint = - `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}${defaultUrlPrefixes.apiRoutePrefix}` as const; + `${mockBackstageApiEndpointWithoutSdkPath}/api/v2` as const; /** * The assets endpoint to use during testing. @@ -82,7 +94,7 @@ export const mockBackstageApiEndpoint = * the final result is. */ export const mockBackstageAssetsEndpoint = - `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}${defaultUrlPrefixes.assetsRoutePrefix}` as const; + `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}` as const; export const mockBearerToken = 'This-is-an-opaque-value-by-design'; export const mockCoderAuthToken = 'ZG0HRy2gGN-mXljc1s5FqtE8WUJ4sUc5X'; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts similarity index 61% rename from plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts rename to plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts index 412e0e05..a3bfb10d 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts @@ -1,21 +1,45 @@ -import type { Workspace } from '../typesConstants'; -import { mockBackstageApiEndpoint } from './mockBackstageData'; +import type { User, Workspace } from '../api/vendoredSdk'; +import { + MockUser, + MockWorkspace, + MockWorkspaceAgent, + MockWorkspaceResource, +} from './coderEntities'; +import { + mockBackstageApiEndpoint, + mockBackstageAssetsEndpoint, +} from './mockBackstageData'; + +export const mockUserWithProxyUrls: User = { + ...MockUser, + avatar_url: `${mockBackstageAssetsEndpoint}/blueberry.png`, +}; /** * The main mock for a workspace whose repo URL matches cleanedRepoUrl */ export const mockWorkspaceWithMatch: Workspace = { + ...MockWorkspace, id: 'workspace-with-match', name: 'Test-Workspace', template_icon: `${mockBackstageApiEndpoint}/emojis/dog.svg`, owner_name: 'lil brudder', + latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-with-match-build', status: 'running', resources: [ { + ...MockWorkspaceResource, id: 'workspace-with-match-resource', - agents: [{ id: 'test-workspace-agent', status: 'connected' }], + agents: [ + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent', + status: 'connected', + }, + ], }, ], }, @@ -28,17 +52,27 @@ export const mockWorkspaceWithMatch: Workspace = { * return multiple values back */ export const mockWorkspaceWithMatch2: Workspace = { + ...MockWorkspace, id: 'workspace-with-match-2', name: 'Another-Test', template_icon: `${mockBackstageApiEndpoint}/emojis/z.svg`, owner_name: 'Coach Z', + latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-with-match-2-build', status: 'running', resources: [ { + ...MockWorkspaceResource, id: 'workspace-with-match-2-resource', - agents: [{ id: 'test-workspace-agent', status: 'connected' }], + agents: [ + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent', + status: 'connected', + }, + ], }, ], }, @@ -49,19 +83,31 @@ export const mockWorkspaceWithMatch2: Workspace = { * cleanedRepoUrl */ export const mockWorkspaceNoMatch: Workspace = { + ...MockWorkspace, id: 'workspace-no-match', name: 'No-match', template_icon: `${mockBackstageApiEndpoint}/emojis/star.svg`, owner_name: 'homestar runner', + latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-no-match-build', status: 'stopped', resources: [ { + ...MockWorkspaceResource, id: 'workspace-no-match-resource', agents: [ - { id: 'test-workspace-agent-a', status: 'disconnected' }, - { id: 'test-workspace-agent-b', status: 'timeout' }, + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-a', + status: 'disconnected', + }, + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-b', + status: 'timeout', + }, ], }, ], @@ -72,17 +118,22 @@ export const mockWorkspaceNoMatch: Workspace = { * A workspace with no build parameters whatsoever */ export const mockWorkspaceNoParameters: Workspace = { + ...MockWorkspace, id: 'workspace-no-parameters', name: 'No-parameters', template_icon: `${mockBackstageApiEndpoint}/emojis/cheese.png`, owner_name: 'The Cheat', latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-no-parameters-build', status: 'running', resources: [ { + ...MockWorkspaceResource, id: 'workspace-no-parameters-resource', - agents: [{ id: 'test-workspace-c', status: 'timeout' }], + agents: [ + { ...MockWorkspaceAgent, id: 'test-workspace-c', status: 'timeout' }, + ], }, ], }, diff --git a/plugins/backstage-plugin-coder/src/testHelpers/server.ts b/plugins/backstage-plugin-coder/src/testHelpers/server.ts index 69fe816a..bacd3f43 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/server.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/server.ts @@ -11,19 +11,18 @@ import { setupServer } from 'msw/node'; /* eslint-enable @backstage/no-undeclared-imports */ import { + mockUserWithProxyUrls, mockWorkspacesList, mockWorkspacesListForRepoSearch, -} from './mockCoderAppData'; +} from './mockCoderPluginData'; import { - mockBackstageAssetsEndpoint, mockBearerToken, mockCoderAuthToken, mockCoderWorkspacesConfig, mockBackstageApiEndpoint as root, } from './mockBackstageData'; -import type { WorkspacesResponse } from '../typesConstants'; import { CODER_AUTH_HEADER_KEY } from '../api/CoderClient'; -import { User } from '../typesConstants'; +import type { User, WorkspacesResponse } from '../api/vendoredSdk'; type RestResolver = ResponseResolver< RestRequest, @@ -83,7 +82,6 @@ export function wrappedGet( export const mockServerEndpoints = { workspaces: `${root}/workspaces`, authenticatedUser: `${root}/users/me`, - workspaceBuildParameters: `${root}/workspacebuilds/:workspaceBuildId/parameters`, } as const satisfies Record; const mainTestHandlers: readonly RestHandler[] = [ @@ -93,7 +91,7 @@ const mainTestHandlers: readonly RestHandler[] = [ `param:"\\w+?=${repoUrl.replace('/', '\\/')}"`, ); - const queryText = String(req.url.searchParams.get('q')); + const queryText = String(req.url.searchParams.get('q') ?? ''); const requestContainsRepoInfo = paramMatcherRe.test(queryText); const baseWorkspaces = requestContainsRepoInfo @@ -129,14 +127,7 @@ const mainTestHandlers: readonly RestHandler[] = [ // This is the dummy request used to verify a user's auth status wrappedGet(mockServerEndpoints.authenticatedUser, (_, res, ctx) => { - return res( - ctx.status(200), - ctx.json({ - id: '1', - avatar_url: `${mockBackstageAssetsEndpoint}/blueberry.png`, - username: 'blueberry', - }), - ); + return res(ctx.status(200), ctx.json(mockUserWithProxyUrls)); }), ]; diff --git a/plugins/backstage-plugin-coder/src/typesConstants.ts b/plugins/backstage-plugin-coder/src/typesConstants.ts index 76551f89..986696bd 100644 --- a/plugins/backstage-plugin-coder/src/typesConstants.ts +++ b/plugins/backstage-plugin-coder/src/typesConstants.ts @@ -1,14 +1,3 @@ -import { - type Output, - array, - number, - object, - string, - union, - literal, - optional, -} from 'valibot'; - export type ReadonlyJsonValue = | string | number @@ -30,80 +19,6 @@ export const CODER_API_REF_ID_PREFIX = 'backstage-plugin-coder'; export const DEFAULT_CODER_DOCS_LINK = 'https://coder.com/docs/v2/latest'; -export const workspaceAgentStatusSchema = union([ - literal('connected'), - literal('connecting'), - literal('disconnected'), - literal('timeout'), -]); - -export const workspaceAgentSchema = object({ - id: string(), - status: workspaceAgentStatusSchema, -}); - -export const workspaceResourceSchema = object({ - id: string(), - agents: optional(array(workspaceAgentSchema)), -}); - -export const workspaceStatusSchema = union([ - literal('canceled'), - literal('canceling'), - literal('deleted'), - literal('deleting'), - literal('failed'), - literal('pending'), - literal('running'), - literal('starting'), - literal('stopped'), - literal('stopping'), -]); - -export const workspaceBuildSchema = object({ - id: string(), - resources: array(workspaceResourceSchema), - status: workspaceStatusSchema, -}); - -export const workspaceSchema = object({ - id: string(), - name: string(), - template_icon: string(), - owner_name: string(), - latest_build: workspaceBuildSchema, -}); - -export const workspacesResponseSchema = object({ - count: number(), - workspaces: array(workspaceSchema), -}); - -export type WorkspaceAgentStatus = Output; -export type WorkspaceAgent = Output; -export type WorkspaceResource = Output; -export type WorkspaceStatus = Output; -export type WorkspaceBuild = Output; -export type Workspace = Output; -export type WorkspacesResponse = Output; - -export type WorkspacesRequest = Readonly<{ - after_id?: string; - limit?: number; - offset?: number; - q?: string; -}>; - -// This is actually the MinimalUser type from Coder core (User extends from -// ReducedUser, which extends from MinimalUser). Don't need all the properties -// until we roll out full SDK support, so going with the least privileged -// type definition for now -export type User = Readonly<{ - id: string; - username: string; - avatar_url: string; -}>; - /** * 2024-05-22 - While this isn't documented anywhere, TanStack Query defaults to * retrying a failed API request 3 times before exposing an error to the UI diff --git a/plugins/backstage-plugin-coder/src/utils/workspaces.ts b/plugins/backstage-plugin-coder/src/utils/workspaces.ts index c36b6d4b..f9317a97 100644 --- a/plugins/backstage-plugin-coder/src/utils/workspaces.ts +++ b/plugins/backstage-plugin-coder/src/utils/workspaces.ts @@ -1,4 +1,4 @@ -import type { Workspace, WorkspaceAgentStatus } from '../typesConstants'; +import { Workspace, WorkspaceAgentStatus } from '../api/vendoredSdk'; export function getWorkspaceAgentStatuses( workspace: Workspace,