Skip to content

Commit cd9f90c

Browse files
authored
refactor(Coder plugin): update workspace queries to use updated API endpoint definitions (#126)
* wip: commit progress on UrlSync class/hook * refactor: consolidate emoji-testing logic * docs: update comments for clarity * refactor: rename helpers to renderHelpers * wip: finish initial implementation of UrlSync * chore: finish tests for UrlSync class * chore: add mock DiscoveryApi helper * chore: finish tests for useUrlSync * refactor: consolidate mock URL logic for useUrlSync * fix: update test helper to use API list * fix: remove unneeded imports * fix: get tests for all current code passing * fix: remove typo * fix: update useUrlSync to expose underlying api * refactor: increase data hiding for hook * fix: make useUrlSync tests less dependent on implementation details * refactor: remove reliance on baseUrl argument for fetch calls * refactor: split Backstage error type into separate file * refactor: clean up imports for api file * refactor: split main query options into separate file * consolidate how mock endpoints are defined * fix: remove base URL from auth calls * refactor: consolidate almost all auth logic into CoderAuthProvider * move api file into api directory * fix: revert prop that was changed for debugging * fix: revert prop definition * refactor: extract token-checking logic into middleware for server * refactor: move shared auth key to queryOptions file * docs: add reminder about arrow functions * wip: add initial versions of CoderClient code * wip: delete entire api.ts file * fix: remove temp api escape hatch for useUrlSync * chore: update syncToken logic to use temporary interceptors * refactor: update variable name for clarity * fix: prevent double-cancellation of timeout signals * fix: cleanup timeout logic * refactor: split pseudo-SDK into separate file * fix: resolve issue with conflicting interceptors * chore: improve cleanup logic * fix: update majority of breaking tests * fix: resolve all breaking tests * fix: beef up CoderClient validation logic * chore: commit first passing test for CoderClient * fix: update error-detection logic in test * wip: add all test stubs for CoderClient * chore: add test cases for syncToken's main return type * chore: add more test cases * fix: remove Object.freeze logic * refactor: consolidate mock API endpoints in one spot * wip: commit current test progress * refactor: rename mock API endpoint variable for clarity * chore: finish test for aborting queued requests * chore: finish initial versions of all CoderClient tests * fix: delete helper that was never used * fix: update getWorkspacesByRepo function signature to be more consistent with base function * docs: add comment reminder about arrow functions for CoderClient * docs: add comment explaining use of interceptor logic * fix: update return type of getWorkspacesByRepo function * fix: finish initial implementation of new API logic * wip: commit progress for updating test setup * fix: update test for CoderClient * fix: update more tests * fix: get all tests passing * chore: remove all build parameter logic * fix: add check for missing key/value for workspaces query
1 parent 74e7dd3 commit cd9f90c

File tree

7 files changed

+92
-111
lines changed

7 files changed

+92
-111
lines changed

plugins/backstage-plugin-coder/src/api/CoderClient.test.ts

+5-14
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { rest } from 'msw';
99
import { mockServerEndpoints, server, wrappedGet } from '../testHelpers/server';
1010
import { CanceledError } from 'axios';
1111
import { delay } from '../utils/time';
12-
import { mockWorkspacesList } from '../testHelpers/mockCoderAppData';
12+
import {
13+
mockWorkspacesList,
14+
mockWorkspacesListForRepoSearch,
15+
} from '../testHelpers/mockCoderAppData';
1316
import type { Workspace, WorkspacesResponse } from '../typesConstants';
1417
import {
1518
getMockConfigApi,
@@ -197,19 +200,7 @@ describe(`${CoderClient.name}`, () => {
197200
mockCoderWorkspacesConfig,
198201
);
199202

200-
const buildParameterGroups = await Promise.all(
201-
workspaces.map(ws =>
202-
client.sdk.getWorkspaceBuildParameters(ws.latest_build.id),
203-
),
204-
);
205-
206-
for (const paramGroup of buildParameterGroups) {
207-
const atLeastOneParamMatchesForGroup = paramGroup.some(param => {
208-
return param.value === mockCoderWorkspacesConfig.repoUrl;
209-
});
210-
211-
expect(atLeastOneParamMatchesForGroup).toBe(true);
212-
}
203+
expect(workspaces).toEqual(mockWorkspacesListForRepoSearch);
213204
});
214205
});
215206
});

plugins/backstage-plugin-coder/src/api/CoderClient.ts

+44-21
Original file line numberDiff line numberDiff line change
@@ -200,37 +200,39 @@ export class CoderClient implements CoderClientApi {
200200
request: WorkspacesRequest,
201201
config: CoderWorkspacesConfig,
202202
): Promise<WorkspacesResponse> => {
203-
const { workspaces } = await baseSdk.getWorkspaces(request);
204-
const paramResults = await Promise.allSettled(
205-
workspaces.map(ws =>
206-
this.sdk.getWorkspaceBuildParameters(ws.latest_build.id),
207-
),
203+
if (config.repoUrl === undefined) {
204+
return { workspaces: [], count: 0 };
205+
}
206+
207+
// Have to store value here so that type information doesn't degrade
208+
// back to (string | undefined) inside the .map callback
209+
const stringUrl = config.repoUrl;
210+
const responses = await Promise.allSettled(
211+
config.repoUrlParamKeys.map(key => {
212+
const patchedRequest = {
213+
...request,
214+
q: appendParamToQuery(request.q, key, stringUrl),
215+
};
216+
217+
return baseSdk.getWorkspaces(patchedRequest);
218+
}),
208219
);
209220

210-
const matchedWorkspaces: Workspace[] = [];
211-
for (const [index, res] of paramResults.entries()) {
221+
const uniqueWorkspaces = new Map<string, Workspace>();
222+
for (const res of responses) {
212223
if (res.status === 'rejected') {
213224
continue;
214225
}
215226

216-
for (const param of res.value) {
217-
const include =
218-
config.repoUrlParamKeys.includes(param.name) &&
219-
param.value === config.repoUrl;
220-
221-
if (include) {
222-
// Doing type assertion just in case noUncheckedIndexedAccess
223-
// compiler setting ever gets turned on; this shouldn't ever break,
224-
// but it's technically not type-safe
225-
matchedWorkspaces.push(workspaces[index] as Workspace);
226-
break;
227-
}
227+
for (const workspace of res.value.workspaces) {
228+
uniqueWorkspaces.set(workspace.id, workspace);
228229
}
229230
}
230231

232+
const serialized = [...uniqueWorkspaces.values()];
231233
return {
232-
workspaces: matchedWorkspaces,
233-
count: matchedWorkspaces.length,
234+
workspaces: serialized,
235+
count: serialized.length,
234236
};
235237
};
236238

@@ -352,6 +354,27 @@ export class CoderClient implements CoderClientApi {
352354
};
353355
}
354356

357+
function appendParamToQuery(
358+
query: string | undefined,
359+
key: string,
360+
value: string,
361+
): string {
362+
if (!key || !value) {
363+
return '';
364+
}
365+
366+
const keyValuePair = `param:"${key}=${value}"`;
367+
if (!query) {
368+
return keyValuePair;
369+
}
370+
371+
if (query.includes(keyValuePair)) {
372+
return query;
373+
}
374+
375+
return `${query} ${keyValuePair}`;
376+
}
377+
355378
function assertValidUser(value: unknown): asserts value is User {
356379
if (value === null || typeof value !== 'object') {
357380
throw new Error('Returned JSON value is not an object');

plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts

-14
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,12 @@ import globalAxios, { type AxiosInstance } from 'axios';
99
import {
1010
type User,
1111
type WorkspacesRequest,
12-
type WorkspaceBuildParameter,
1312
type WorkspacesResponse,
1413
} from '../typesConstants';
1514

1615
type CoderSdkApi = {
1716
getAuthenticatedUser: () => Promise<User>;
1817
getWorkspaces: (request: WorkspacesRequest) => Promise<WorkspacesResponse>;
19-
getWorkspaceBuildParameters: (
20-
workspaceBuildId: string,
21-
) => Promise<readonly WorkspaceBuildParameter[]>;
2218
};
2319

2420
export class CoderSdk implements CoderSdkApi {
@@ -45,16 +41,6 @@ export class CoderSdk implements CoderSdkApi {
4541
return response.data;
4642
};
4743

48-
getWorkspaceBuildParameters = async (
49-
workspaceBuildId: string,
50-
): Promise<readonly WorkspaceBuildParameter[]> => {
51-
const response = await this.axios.get<readonly WorkspaceBuildParameter[]>(
52-
`/workspacebuilds/${workspaceBuildId}/parameters`,
53-
);
54-
55-
return response.data;
56-
};
57-
5844
getAuthenticatedUser = async (): Promise<User> => {
5945
const response = await this.axios.get<User>('/users/me');
6046
return response.data;

plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ describe(`${CoderWorkspacesCard.name}`, () => {
7373

7474
await user.tripleClick(searchbox);
7575
await user.keyboard('[Backspace]');
76-
await user.keyboard('I can do it - I can do it nine times');
76+
await user.keyboard('I-can-do-it-I-can-do-it-nine-times');
7777

7878
await waitFor(() => {
7979
// getAllByRole will throw if there isn't at least one node matched
@@ -153,12 +153,12 @@ describe(`${CoderWorkspacesCard.name}`, () => {
153153
});
154154

155155
/**
156-
* 2024-03-28 - MES - This is a test case to account for a previous
157-
* limitation around querying workspaces by repo URL.
156+
* For performance reasons, the queries for getting workspaces by repo are
157+
* disabled when the query string is empty.
158158
*
159-
* This limitation no longer exists, so this test should be removed once the
160-
* rest of the codebase is updated to support the new API endpoint for
161-
* searching by build parameter
159+
* Even with the API endpoint for searching workspaces by build parameter,
160+
* you still have to shoot off a bunch of requests just to find everything
161+
* that could possibly match your Backstage deployment's config options.
162162
*/
163163
it('Will not show any workspaces at all when the query text is empty', async () => {
164164
await renderWorkspacesCard({ readEntityData: true });

plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts

+6-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { Workspace, WorkspaceBuildParameter } from '../typesConstants';
2-
import { cleanedRepoUrl, mockBackstageApiEndpoint } from './mockBackstageData';
1+
import type { Workspace } from '../typesConstants';
2+
import { mockBackstageApiEndpoint } from './mockBackstageData';
33

44
/**
55
* The main mock for a workspace whose repo URL matches cleanedRepoUrl
@@ -98,23 +98,7 @@ export const mockWorkspacesList: Workspace[] = [
9898
mockWorkspaceNoParameters,
9999
];
100100

101-
export const mockWorkspaceBuildParameters: Record<
102-
string,
103-
readonly WorkspaceBuildParameter[]
104-
> = {
105-
[mockWorkspaceWithMatch.latest_build.id]: [
106-
{ name: 'repo_url', value: cleanedRepoUrl },
107-
],
108-
109-
[mockWorkspaceWithMatch2.latest_build.id]: [
110-
{ name: 'repo_url', value: cleanedRepoUrl },
111-
],
112-
113-
[mockWorkspaceNoMatch.latest_build.id]: [
114-
{ name: 'repo_url', value: 'https://www.github.com/wombo/zom' },
115-
],
116-
117-
[mockWorkspaceNoParameters.latest_build.id]: [
118-
// Intentionally kept empty
119-
],
120-
};
101+
export const mockWorkspacesListForRepoSearch: Workspace[] = [
102+
mockWorkspaceWithMatch,
103+
mockWorkspaceWithMatch2,
104+
];

plugins/backstage-plugin-coder/src/testHelpers/server.ts

+31-22
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ import { setupServer } from 'msw/node';
1212

1313
import {
1414
mockWorkspacesList,
15-
mockWorkspaceBuildParameters,
15+
mockWorkspacesListForRepoSearch,
1616
} from './mockCoderAppData';
1717
import {
1818
mockBackstageAssetsEndpoint,
1919
mockBearerToken,
2020
mockCoderAuthToken,
21+
mockCoderWorkspacesConfig,
2122
mockBackstageApiEndpoint as root,
2223
} from './mockBackstageData';
23-
import type { Workspace, WorkspacesResponse } from '../typesConstants';
24+
import type { WorkspacesResponse } from '../typesConstants';
2425
import { CODER_AUTH_HEADER_KEY } from '../api/CoderClient';
2526
import { User } from '../typesConstants';
2627

@@ -87,37 +88,45 @@ export const mockServerEndpoints = {
8788

8889
const mainTestHandlers: readonly RestHandler[] = [
8990
wrappedGet(mockServerEndpoints.workspaces, (req, res, ctx) => {
90-
const queryText = String(req.url.searchParams.get('q'));
91+
const { repoUrl } = mockCoderWorkspacesConfig;
92+
const paramMatcherRe = new RegExp(
93+
`param:"\\w+?=${repoUrl.replace('/', '\\/')}"`,
94+
);
9195

92-
let returnedWorkspaces: Workspace[];
93-
if (queryText === 'owner:me') {
94-
returnedWorkspaces = mockWorkspacesList;
95-
} else {
96-
returnedWorkspaces = mockWorkspacesList.filter(ws =>
97-
ws.name.includes(queryText),
96+
const queryText = String(req.url.searchParams.get('q'));
97+
const requestContainsRepoInfo = paramMatcherRe.test(queryText);
98+
99+
const baseWorkspaces = requestContainsRepoInfo
100+
? mockWorkspacesListForRepoSearch
101+
: mockWorkspacesList;
102+
103+
const customSearchTerms = queryText
104+
.split(' ')
105+
.filter(text => text !== 'owner:me' && !paramMatcherRe.test(text));
106+
107+
if (customSearchTerms.length === 0) {
108+
return res(
109+
ctx.status(200),
110+
ctx.json<WorkspacesResponse>({
111+
workspaces: baseWorkspaces,
112+
count: baseWorkspaces.length,
113+
}),
98114
);
99115
}
100116

117+
const filtered = mockWorkspacesList.filter(ws => {
118+
return customSearchTerms.some(term => ws.name.includes(term));
119+
});
120+
101121
return res(
102122
ctx.status(200),
103123
ctx.json<WorkspacesResponse>({
104-
workspaces: returnedWorkspaces,
105-
count: returnedWorkspaces.length,
124+
workspaces: filtered,
125+
count: filtered.length,
106126
}),
107127
);
108128
}),
109129

110-
wrappedGet(mockServerEndpoints.workspaceBuildParameters, (req, res, ctx) => {
111-
const buildId = String(req.params.workspaceBuildId);
112-
const selectedParams = mockWorkspaceBuildParameters[buildId];
113-
114-
if (selectedParams !== undefined) {
115-
return res(ctx.status(200), ctx.json(selectedParams));
116-
}
117-
118-
return res(ctx.status(404));
119-
}),
120-
121130
// This is the dummy request used to verify a user's auth status
122131
wrappedGet(mockServerEndpoints.authenticatedUser, (_, res, ctx) => {
123132
return res(

plugins/backstage-plugin-coder/src/typesConstants.ts

-12
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,6 @@ export const workspaceSchema = object({
7474
latest_build: workspaceBuildSchema,
7575
});
7676

77-
export const workspaceBuildParameterSchema = object({
78-
name: string(),
79-
value: string(),
80-
});
81-
82-
export const workspaceBuildParametersSchema = array(
83-
workspaceBuildParameterSchema,
84-
);
85-
8677
export const workspacesResponseSchema = object({
8778
count: number(),
8879
workspaces: array(workspaceSchema),
@@ -95,9 +86,6 @@ export type WorkspaceStatus = Output<typeof workspaceStatusSchema>;
9586
export type WorkspaceBuild = Output<typeof workspaceBuildSchema>;
9687
export type Workspace = Output<typeof workspaceSchema>;
9788
export type WorkspacesResponse = Output<typeof workspacesResponseSchema>;
98-
export type WorkspaceBuildParameter = Output<
99-
typeof workspaceBuildParameterSchema
100-
>;
10189

10290
export type WorkspacesRequest = Readonly<{
10391
after_id?: string;

0 commit comments

Comments
 (0)