diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts index 945d8317..9addcd1a 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts @@ -9,7 +9,10 @@ import { rest } from 'msw'; import { mockServerEndpoints, server, wrappedGet } from '../testHelpers/server'; import { CanceledError } from 'axios'; import { delay } from '../utils/time'; -import { mockWorkspacesList } from '../testHelpers/mockCoderAppData'; +import { + mockWorkspacesList, + mockWorkspacesListForRepoSearch, +} from '../testHelpers/mockCoderAppData'; import type { Workspace, WorkspacesResponse } from '../typesConstants'; import { getMockConfigApi, @@ -197,19 +200,7 @@ describe(`${CoderClient.name}`, () => { mockCoderWorkspacesConfig, ); - const buildParameterGroups = await Promise.all( - workspaces.map(ws => - client.sdk.getWorkspaceBuildParameters(ws.latest_build.id), - ), - ); - - for (const paramGroup of buildParameterGroups) { - const atLeastOneParamMatchesForGroup = paramGroup.some(param => { - return param.value === mockCoderWorkspacesConfig.repoUrl; - }); - - expect(atLeastOneParamMatchesForGroup).toBe(true); - } + expect(workspaces).toEqual(mockWorkspacesListForRepoSearch); }); }); }); diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.ts index 047c08ca..7c09f72c 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.ts @@ -200,37 +200,39 @@ export class CoderClient implements CoderClientApi { request: WorkspacesRequest, config: CoderWorkspacesConfig, ): Promise => { - const { workspaces } = await baseSdk.getWorkspaces(request); - const paramResults = await Promise.allSettled( - workspaces.map(ws => - this.sdk.getWorkspaceBuildParameters(ws.latest_build.id), - ), + if (config.repoUrl === undefined) { + return { workspaces: [], count: 0 }; + } + + // Have to store value here so that type information doesn't degrade + // back to (string | undefined) inside the .map callback + const stringUrl = config.repoUrl; + const responses = await Promise.allSettled( + config.repoUrlParamKeys.map(key => { + const patchedRequest = { + ...request, + q: appendParamToQuery(request.q, key, stringUrl), + }; + + return baseSdk.getWorkspaces(patchedRequest); + }), ); - const matchedWorkspaces: Workspace[] = []; - for (const [index, res] of paramResults.entries()) { + const uniqueWorkspaces = new Map(); + for (const res of responses) { if (res.status === 'rejected') { continue; } - for (const param of res.value) { - const include = - config.repoUrlParamKeys.includes(param.name) && - param.value === config.repoUrl; - - if (include) { - // Doing type assertion just in case noUncheckedIndexedAccess - // compiler setting ever gets turned on; this shouldn't ever break, - // but it's technically not type-safe - matchedWorkspaces.push(workspaces[index] as Workspace); - break; - } + for (const workspace of res.value.workspaces) { + uniqueWorkspaces.set(workspace.id, workspace); } } + const serialized = [...uniqueWorkspaces.values()]; return { - workspaces: matchedWorkspaces, - count: matchedWorkspaces.length, + workspaces: serialized, + count: serialized.length, }; }; @@ -352,6 +354,27 @@ export class CoderClient implements CoderClientApi { }; } +function appendParamToQuery( + query: string | undefined, + key: string, + value: string, +): string { + if (!key || !value) { + return ''; + } + + const keyValuePair = `param:"${key}=${value}"`; + if (!query) { + return keyValuePair; + } + + if (query.includes(keyValuePair)) { + return query; + } + + return `${query} ${keyValuePair}`; +} + function assertValidUser(value: unknown): asserts value is User { if (value === null || typeof value !== 'object') { throw new Error('Returned JSON value is not an object'); diff --git a/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts b/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts index 4245a65a..3100242b 100644 --- a/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts +++ b/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts @@ -9,16 +9,12 @@ import globalAxios, { type AxiosInstance } from 'axios'; import { type User, type WorkspacesRequest, - type WorkspaceBuildParameter, type WorkspacesResponse, } from '../typesConstants'; type CoderSdkApi = { getAuthenticatedUser: () => Promise; getWorkspaces: (request: WorkspacesRequest) => Promise; - getWorkspaceBuildParameters: ( - workspaceBuildId: string, - ) => Promise; }; export class CoderSdk implements CoderSdkApi { @@ -45,16 +41,6 @@ export class CoderSdk implements CoderSdkApi { return response.data; }; - getWorkspaceBuildParameters = async ( - workspaceBuildId: string, - ): Promise => { - const response = await this.axios.get( - `/workspacebuilds/${workspaceBuildId}/parameters`, - ); - - 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/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx index b99a9d69..a8cbef6c 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx @@ -73,7 +73,7 @@ describe(`${CoderWorkspacesCard.name}`, () => { await user.tripleClick(searchbox); await user.keyboard('[Backspace]'); - await user.keyboard('I can do it - I can do it nine times'); + await user.keyboard('I-can-do-it-I-can-do-it-nine-times'); await waitFor(() => { // getAllByRole will throw if there isn't at least one node matched @@ -153,12 +153,12 @@ describe(`${CoderWorkspacesCard.name}`, () => { }); /** - * 2024-03-28 - MES - This is a test case to account for a previous - * limitation around querying workspaces by repo URL. + * For performance reasons, the queries for getting workspaces by repo are + * disabled when the query string is empty. * - * This limitation no longer exists, so this test should be removed once the - * rest of the codebase is updated to support the new API endpoint for - * searching by build parameter + * Even with the API endpoint for searching workspaces by build parameter, + * you still have to shoot off a bunch of requests just to find everything + * that could possibly match your Backstage deployment's config options. */ it('Will not show any workspaces at all when the query text is empty', async () => { await renderWorkspacesCard({ readEntityData: true }); diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts index ce63590f..412e0e05 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts @@ -1,5 +1,5 @@ -import type { Workspace, WorkspaceBuildParameter } from '../typesConstants'; -import { cleanedRepoUrl, mockBackstageApiEndpoint } from './mockBackstageData'; +import type { Workspace } from '../typesConstants'; +import { mockBackstageApiEndpoint } from './mockBackstageData'; /** * The main mock for a workspace whose repo URL matches cleanedRepoUrl @@ -98,23 +98,7 @@ export const mockWorkspacesList: Workspace[] = [ mockWorkspaceNoParameters, ]; -export const mockWorkspaceBuildParameters: Record< - string, - readonly WorkspaceBuildParameter[] -> = { - [mockWorkspaceWithMatch.latest_build.id]: [ - { name: 'repo_url', value: cleanedRepoUrl }, - ], - - [mockWorkspaceWithMatch2.latest_build.id]: [ - { name: 'repo_url', value: cleanedRepoUrl }, - ], - - [mockWorkspaceNoMatch.latest_build.id]: [ - { name: 'repo_url', value: 'https://www.github.com/wombo/zom' }, - ], - - [mockWorkspaceNoParameters.latest_build.id]: [ - // Intentionally kept empty - ], -}; +export const mockWorkspacesListForRepoSearch: Workspace[] = [ + mockWorkspaceWithMatch, + mockWorkspaceWithMatch2, +]; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/server.ts b/plugins/backstage-plugin-coder/src/testHelpers/server.ts index 47751269..69fe816a 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/server.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/server.ts @@ -12,15 +12,16 @@ import { setupServer } from 'msw/node'; import { mockWorkspacesList, - mockWorkspaceBuildParameters, + mockWorkspacesListForRepoSearch, } from './mockCoderAppData'; import { mockBackstageAssetsEndpoint, mockBearerToken, mockCoderAuthToken, + mockCoderWorkspacesConfig, mockBackstageApiEndpoint as root, } from './mockBackstageData'; -import type { Workspace, WorkspacesResponse } from '../typesConstants'; +import type { WorkspacesResponse } from '../typesConstants'; import { CODER_AUTH_HEADER_KEY } from '../api/CoderClient'; import { User } from '../typesConstants'; @@ -87,37 +88,45 @@ export const mockServerEndpoints = { const mainTestHandlers: readonly RestHandler[] = [ wrappedGet(mockServerEndpoints.workspaces, (req, res, ctx) => { - const queryText = String(req.url.searchParams.get('q')); + const { repoUrl } = mockCoderWorkspacesConfig; + const paramMatcherRe = new RegExp( + `param:"\\w+?=${repoUrl.replace('/', '\\/')}"`, + ); - let returnedWorkspaces: Workspace[]; - if (queryText === 'owner:me') { - returnedWorkspaces = mockWorkspacesList; - } else { - returnedWorkspaces = mockWorkspacesList.filter(ws => - ws.name.includes(queryText), + const queryText = String(req.url.searchParams.get('q')); + const requestContainsRepoInfo = paramMatcherRe.test(queryText); + + const baseWorkspaces = requestContainsRepoInfo + ? mockWorkspacesListForRepoSearch + : mockWorkspacesList; + + const customSearchTerms = queryText + .split(' ') + .filter(text => text !== 'owner:me' && !paramMatcherRe.test(text)); + + if (customSearchTerms.length === 0) { + return res( + ctx.status(200), + ctx.json({ + workspaces: baseWorkspaces, + count: baseWorkspaces.length, + }), ); } + const filtered = mockWorkspacesList.filter(ws => { + return customSearchTerms.some(term => ws.name.includes(term)); + }); + return res( ctx.status(200), ctx.json({ - workspaces: returnedWorkspaces, - count: returnedWorkspaces.length, + workspaces: filtered, + count: filtered.length, }), ); }), - wrappedGet(mockServerEndpoints.workspaceBuildParameters, (req, res, ctx) => { - const buildId = String(req.params.workspaceBuildId); - const selectedParams = mockWorkspaceBuildParameters[buildId]; - - if (selectedParams !== undefined) { - return res(ctx.status(200), ctx.json(selectedParams)); - } - - return res(ctx.status(404)); - }), - // This is the dummy request used to verify a user's auth status wrappedGet(mockServerEndpoints.authenticatedUser, (_, res, ctx) => { return res( diff --git a/plugins/backstage-plugin-coder/src/typesConstants.ts b/plugins/backstage-plugin-coder/src/typesConstants.ts index 788a2dba..d9922920 100644 --- a/plugins/backstage-plugin-coder/src/typesConstants.ts +++ b/plugins/backstage-plugin-coder/src/typesConstants.ts @@ -74,15 +74,6 @@ export const workspaceSchema = object({ latest_build: workspaceBuildSchema, }); -export const workspaceBuildParameterSchema = object({ - name: string(), - value: string(), -}); - -export const workspaceBuildParametersSchema = array( - workspaceBuildParameterSchema, -); - export const workspacesResponseSchema = object({ count: number(), workspaces: array(workspaceSchema), @@ -95,9 +86,6 @@ export type WorkspaceStatus = Output; export type WorkspaceBuild = Output; export type Workspace = Output; export type WorkspacesResponse = Output; -export type WorkspaceBuildParameter = Output< - typeof workspaceBuildParameterSchema ->; export type WorkspacesRequest = Readonly<{ after_id?: string;