Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add missing unit tests for Coder plugin #83

Merged
merged 85 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 81 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
3590f19
fix: fix hover behavior for last list item
Parkreiner Mar 13, 2024
8eb2b5a
fix: shrink default max height for container
Parkreiner Mar 13, 2024
4680af4
fix: ensure divider bar appears when there is overflow
Parkreiner Mar 13, 2024
45607c6
refactor: add workspaceCreationLink prop to context provider
Parkreiner Mar 13, 2024
aec0baa
refactor: split Placeholder into separate component
Parkreiner Mar 13, 2024
6ecaa0a
chore: finish cta button
Parkreiner Mar 13, 2024
c9702e3
fix: make sure button only appears when loading is finished
Parkreiner Mar 13, 2024
cbb9897
docs: remove bad comment
Parkreiner Mar 13, 2024
7daaded
chore: add explicit return type to useCoderAppConfig for clarity
Parkreiner Mar 13, 2024
3536a59
refactor: consolidate and decouple type definitions
Parkreiner Mar 13, 2024
0653eab
refactor: move dynamic entity config logic
Parkreiner Mar 13, 2024
4983cdd
Merge branch 'main' into mes/template-name-update
Parkreiner Mar 13, 2024
1d931fe
refactor: update references for workspaces config
Parkreiner Mar 13, 2024
1d809aa
refactor: centralize creationUrl logic
Parkreiner Mar 13, 2024
af5e4b1
refactor: rename useCoderEntityConfig to useCoderWorkspacesConfig
Parkreiner Mar 13, 2024
a4f3951
refactor: rename old useCoderWorkspaces to useCoderWorkspacesQuery
Parkreiner Mar 13, 2024
9636010
fix: update typo in test case
Parkreiner Mar 13, 2024
759ee1c
fix: update test logic to account for creationUrl
Parkreiner Mar 13, 2024
3347be1
fix: update query logic to account for always-defined workspacesConfig
Parkreiner Mar 13, 2024
535a679
docs: fix typo in comment
Parkreiner Mar 13, 2024
097e481
refactor: clean up how mock data is defined
Parkreiner Mar 14, 2024
bf9cf66
Merge branch 'main' into mes/template-name-update
Parkreiner Mar 15, 2024
7927ffe
fix: make logic for showing reminder more airtight
Parkreiner Mar 15, 2024
858b69e
refactor: split DataReminder into separate file
Parkreiner Mar 15, 2024
947a1cd
Merge branch 'main' into mes/template-name-update
Parkreiner Mar 23, 2024
ca1ffba
refactor: simplify API for useCoderWorkspacesQuery
Parkreiner Mar 23, 2024
7bd2ac6
fix: make sure data reminder only shows when appropriate
Parkreiner Mar 23, 2024
604c04c
wip: commit progress on auth test
Parkreiner Mar 23, 2024
9026a80
chore: simplify setup for CoderProviderWithMockAuth
Parkreiner Mar 23, 2024
6ff277a
wip: reorganize test structure
Parkreiner Mar 23, 2024
55c37fb
chore: update test helper to accept mock callbacks
Parkreiner Mar 23, 2024
8735a29
wip: commit more test progress
Parkreiner Mar 23, 2024
70a1b86
chore: finish tests for CoderAuthWrapper
Parkreiner Mar 23, 2024
3e77397
refactor: make error message more user-friendly
Parkreiner Mar 23, 2024
901f34c
fix: delete stale DataReminder file
Parkreiner Mar 23, 2024
8540834
Merge branch 'mes/template-name-update' into mes/workspace-tests
Parkreiner Mar 23, 2024
1654436
fix: delete untested test helpers
Parkreiner Mar 23, 2024
e3f93fa
chore: finish tests for CreateWorkspaceLink
Parkreiner Mar 23, 2024
b4b5f6f
refactor: extract test setup logic into helper
Parkreiner Mar 23, 2024
e360341
chore: finish tests for EntityDataReminder
Parkreiner Mar 23, 2024
ed53123
fix: update indenting level for comment
Parkreiner Mar 24, 2024
7c818a1
wip: add stubs for ExtraActionsButton
Parkreiner Mar 24, 2024
804abdf
refactor: update APIs for test helpers
Parkreiner Mar 24, 2024
438f681
chore: add test case for submitting new token
Parkreiner Mar 24, 2024
e3d421d
chore: let user pass in custom query client for test helper
Parkreiner Mar 24, 2024
a867b82
wip: lay out test stubs for ExtraActionsButton
Parkreiner Mar 24, 2024
5bbb510
refactor: simplify API for renderInCoderEnvironment
Parkreiner Mar 25, 2024
9495ce0
wip: commit progress on ExtraActionsButton
Parkreiner Mar 25, 2024
d6c3233
wip: add test case for keyboard input
Parkreiner Mar 25, 2024
8fc9c13
fix: make better assertions about auto-focus
Parkreiner Mar 25, 2024
3c9cf87
chore: finish tests for ExtraActionsButton
Parkreiner Mar 25, 2024
3eb5fc2
chore: update test helper to accept custom entity
Parkreiner Mar 25, 2024
95a1f6c
chore: add mock repo name value
Parkreiner Mar 25, 2024
dce4218
wip: commit current progress on HeaderRow tests
Parkreiner Mar 25, 2024
ee3eb77
chore: finish tests for HeaderRow
Parkreiner Mar 25, 2024
868d6e8
docs: update comment for clarity
Parkreiner Mar 25, 2024
ccae871
fix: update repo URL parsing logic
Parkreiner Mar 25, 2024
3a31459
chore: add test for Placeholder
Parkreiner Mar 25, 2024
b8c3681
refactor: update type definitions for ExtraActionsButton test
Parkreiner Mar 25, 2024
ea121cd
wip: add stub logic for SearchBox tests
Parkreiner Mar 25, 2024
d4a284c
docs: add note about how Root probably shouldn't be tested
Parkreiner Mar 25, 2024
151bc69
wip: add stubs for CoderWorkspacesCard tests
Parkreiner Mar 25, 2024
cbdc9bc
wip: reorganize test cases
Parkreiner Mar 25, 2024
b8ed45e
chore: finish initial draft of tests for SearchBox
Parkreiner Mar 26, 2024
65b53c2
chore: update test logic to account for debounces
Parkreiner Mar 26, 2024
092032f
fix: update test to account for throttles better
Parkreiner Mar 26, 2024
8be859e
wip: commit progress for Root tests
Parkreiner Mar 26, 2024
4264ce8
chore: finish tests for Root
Parkreiner Mar 26, 2024
6b43cdf
wip: commit progress on WorkspacesListIcon test
Parkreiner Mar 26, 2024
39cbff6
refactor: update WorkspacesListIcon to be easier to test
Parkreiner Mar 26, 2024
8d6dc4c
chore: finish tests for WorkspacesListIcon
Parkreiner Mar 26, 2024
a3e3771
chore: add tests for WorkspacesListItem
Parkreiner Mar 26, 2024
6334ef3
docs: add note about scope of tests for Root
Parkreiner Mar 27, 2024
8c2f9da
chore: finish tests for WorkspacesListItem
Parkreiner Mar 27, 2024
425923b
chore: finish all unit tests
Parkreiner Mar 27, 2024
0b63640
fix: delete empty test file (to be added in future PR)
Parkreiner Mar 27, 2024
8af8e57
docs: update type definitions
Parkreiner Mar 27, 2024
953c79a
docs: update hook/type docs to reflect new APIs
Parkreiner Mar 27, 2024
de61a17
docs: fix typo
Parkreiner Mar 27, 2024
d33441e
Merge branch 'main' into mes/template-name-update
Parkreiner Mar 27, 2024
48cde6c
Merge branch 'mes/template-name-update' into mes/workspace-tests
Parkreiner Mar 27, 2024
914001e
chore: try removing react-use dependency to make CI happy
Parkreiner Mar 27, 2024
6203120
Merge branch 'main' into mes/template-name-update
Parkreiner Mar 28, 2024
ea056be
Merge branch 'mes/template-name-update' into mes/workspace-tests
Parkreiner Mar 28, 2024
ffb1d02
merge main into mes/workspace-tests
Parkreiner Mar 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions plugins/backstage-plugin-coder/docs/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ declare function CoderAuthWrapper(props: Props): JSX.Element;
```tsx
function YourComponent() {
// This query requires authentication
const query = useCoderWorkspaces('owner:lil-brudder');
return <p>{query.isLoading ? 'Loading' : 'Not loading'}</p>;
const queryState = useCoderWorkspacesQuery({
coderQuery: 'owner:lil-brudder',
});

return <p>{queryState.isLoading ? 'Loading' : 'Not loading'}</p>;
}

<CoderProvider appConfig={yourAppConfig}>
Expand Down Expand Up @@ -79,7 +82,7 @@ declare function CoderErrorBoundary(props: Props): JSX.Element;
function YourComponent() {
// Pretend that there is an issue with this hook, and that it will always
// throw an error
const config = useCoderEntityConfig();
const config = useCoderWorkspacesConfig();
return <p>Will never reach this code</p>;
}

Expand Down Expand Up @@ -123,10 +126,13 @@ The type of `QueryClient` comes from [Tanstack Router v4](https://tanstack.com/q

```tsx
function YourComponent() {
const query = useCoderWorkspaces('owner:brennan-lee-mulligan');
const queryState = useCoderWorkspacesQuery({
coderQuery: 'owner:brennan-lee-mulligan',
});

return (
<ul>
{query.data?.map(workspace => (
{queryState.data?.map(workspace => (
<li key={workspace.id}>{workspace.owner_name}</li>
))}
</ul>
Expand Down Expand Up @@ -396,8 +402,8 @@ type WorkspacesCardContext = {
queryFilter: string;
onFilterChange: (newFilter: string) => void;
workspacesQuery: UseQueryResult<readonly Workspace[]>;
workspacesConfig: CoderWorkspacesConfig;
headerId: string;
entityConfig: CoderEntityConfig | undefined;
};

declare function Root(props: Props): JSX.Element;
Expand Down
63 changes: 33 additions & 30 deletions plugins/backstage-plugin-coder/docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,33 @@ This is the main documentation page for the Coder plugin's React hooks.

## Hook list

- [`useCoderEntityConfig`](#useCoderEntityConfig)
- [`useCoderWorkspaces`](#useCoderWorkspaces)
- [`useCoderWorkspacesConfig`](#useCoderWorkspacesConfig)
- [`useCoderWorkspacesQuery`](#useCoderWorkspacesquery)
- [`useWorkspacesCardContext`](#useWorkspacesCardContext)

## `useCoderEntityConfig`
## `useCoderWorkspacesConfig`

This hook gives you access to compiled [`CoderEntityConfig`](./types.md#coderentityconfig) data.
This hook gives you access to compiled [`CoderWorkspacesConfig`](./types.md#coderworkspacesconfig) data.

### Type signature

```tsx
declare function useCoderEntityConfig(): CoderEntityConfig;
type UseCoderWorkspacesConfigOptions = Readonly<{
readEntityData?: boolean;
}>;

declare function useCoderWorkspacesConfig(
options: UseCoderWorkspacesConfigOptions,
): CoderWorkspacesConfig;
```

[Type definition for `CoderEntityConfig`](./types.md#coderentityconfig)
[Type definition for `CoderWorkspacesConfig`](./types.md#coderWorkspacesconfig)

### Example usage

```tsx
function YourComponent() {
const config = useCoderEntityConfig();
const config = useCoderWorkspacesConfig();
return <p>Your repo URL is {config.repoUrl}</p>;
}

Expand Down Expand Up @@ -52,50 +58,46 @@ const serviceEntityPage = (
### Throws

- Will throw an error if called outside a React component
- Will throw an error if called outside an `EntityLayout` (or any other Backstage component that exposes `Entity` data via React Context)
- Will throw if the value of the `readEntityData` property input changes across re-renders

### Notes

- The type definition for `CoderEntityConfig` [can be found here](./types.md#coderentityconfig). That section also includes info on the heuristic used for compiling the data
- The type definition for `CoderWorkspacesConfig` [can be found here](./types.md#coderworkspacesconfig). That section also includes info on the heuristic used for compiling the data
- The value of `readEntityData` determines the "mode" that the workspace operates in. If the value is `false`/`undefined`, the component will act as a general list of workspaces that isn't aware of Backstage APIs. If the value is `true`, the hook will also read Backstage data during the compilation step.
- The hook tries to ensure that the returned value maintains a stable memory reference as much as possible, if you ever need to use that value in other React hooks that use dependency arrays (e.g., `useEffect`, `useCallback`)

## `useCoderWorkspaces`
## `useCoderWorkspacesQuery`

This hook gives you access to all workspaces that match a given query string. If
[`repoConfig`](#usecoderentityconfig) is defined via `options`, the workspaces returned will be filtered down further to only those that match the the repo.
[`workspacesConfig`](#usecoderworkspacesconfig) is defined via `options`, and that config has a defined `repoUrl`, the workspaces returned will be filtered down further to only those that match the the repo.

### Type signature

```ts
type UseCoderWorkspacesOptions = Readonly<
Partial<{
repoConfig: CoderEntityConfig;
}>
>;

declare function useCoderEntityConfig(
coderQuery: string,
options?: UseCoderWorkspacesOptions,
type UseCoderWorkspacesQueryOptions = Readonly<{
coderQuery: string;
workspacesConfig?: CoderWorkspacesConfig;
}>;

declare function useCoderWorkspacesConfig(
options: UseCoderWorkspacesQueryOptions,
): UseQueryResult<readonly Workspace[]>;
```

### Example usage

```tsx
function YourComponent() {
const entityConfig = useCoderEntityConfig();
const [filter, setFilter] = useState('owner:me');

const query = useCoderWorkspaces(filter, {
repoConfig: entityConfig,
});
const workspacesConfig = useCoderWorkspacesConfig({ readEntityData: true });
const queryState = useCoderWorkspacesQuery({ filter, workspacesConfig });

return (
<>
{query.isLoading && <YourLoadingIndicator />}
{query.isError && <YourErrorDisplay />}
{queryState.isLoading && <YourLoadingIndicator />}
{queryState.isError && <YourErrorDisplay />}

{query.data?.map(workspace => (
{queryState.data?.map(workspace => (
<ol>
<li key={workspace.key}>{workspace.name}</li>
</ol>
Expand Down Expand Up @@ -127,7 +129,8 @@ const coderAppConfig: CoderAppConfig = {
- The underlying query will not be enabled if:
1. The user is not currently authenticated (We recommend wrapping your component inside [`CoderAuthWrapper`](./components.md#coderauthwrapper) to make these checks easier)
2. If `repoConfig` is passed in via `options`: when the value of `coderQuery` is an empty string
- `CoderEntityConfig` is the return type of [`useCoderEntityConfig`](#usecoderentityconfig)
- The `workspacesConfig` property is the return type of [`useCoderWorkspacesConfig`](#usecoderworkspacesconfig)
- The only way to get automatically-filtered results is by (1) passing in a workspaces config value, and (2) ensuring that config has a `repoUrl` property of type string (it can sometimes be `undefined`, depending on built-in Backstage APIs).

## `useWorkspacesCardContext`

Expand All @@ -140,8 +143,8 @@ type WorkspacesCardContext = Readonly<{
queryFilter: string;
onFilterChange: (newFilter: string) => void;
workspacesQuery: UseQueryResult<readonly Workspace[]>;
workspacesConfig: CoderWorkspacesConfig;
headerId: string;
entityConfig: CoderEntityConfig | undefined;
}>;

declare function useWorkspacesCardContext(): WorkspacesCardContext;
Expand Down
31 changes: 19 additions & 12 deletions plugins/backstage-plugin-coder/docs/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@

```tsx
// Type intersection
type CustomType = CoderEntityConfig & {
type CustomType = CoderWorkspacesConfig & {
customProperty: boolean;
};

// Interface extension - new interface must have a different name
interface CustomInterface extends CoderEntityConfig {
interface CustomInterface extends CoderWorkspacesConfig {
customProperty: string;
}
```

## Types directory

- [`CoderAppConfig`](#coderappconfig)
- [`CoderEntityConfig`](#coderentityconfig)
- [`CoderWorkspacesConfig`](#coderworkspacesconfig)
- [`Workspace`](#workspace)
- [`WorkspaceResponse`](#workspaceresponse)

Expand Down Expand Up @@ -57,22 +57,23 @@ See example for [`CoderProvider`](./components.md#coderprovider)
- `templateName` refers to the name of the Coder template that you wish to use as default for creating workspaces
- If `mode` is not specified, the plugin will default to a value of `manual`
- `repoUrlParamKeys` is defined as a non-empty array – there must be at least one element inside it.
- For more info on how this type is used within the plugin, see [`CoderEntityConfig`](./types.md#coderentityconfig) and [`useCoderEntityConfig`](./hooks.md#usecoderentityconfig)
- For more info on how this type is used within the plugin, see [`CoderWorkspacesConfig`](./types.md#coderworkspacesconfig) and [`useCoderWorkspacesConfig`](./hooks.md#usecoderworkspacesconfig)

## `CoderEntityConfig`
## `CoderWorkspacesConfig`

Represents the result of compiling Coder plugin configuration data. All data will be compiled from the following sources:
Represents the result of compiling Coder plugin configuration data. The main source for this type is [`useCoderWorkspacesConfig`](./hooks.md#usecoderworkspacesconfig). All data will be compiled from the following sources:

1. The [`CoderAppConfig`](#coderappconfig) passed to [`CoderProvider`](./components.md#coderprovider)
1. The [`CoderAppConfig`](#coderappconfig) passed to [`CoderProvider`](./components.md#coderprovider). This acts as the "baseline" set of values.
2. The entity-specific fields for a given repo's `catalog-info.yaml` file
3. The entity's location metadata (corresponding to the repo)

### Type definition

```tsx
type CoderEntityConfig = Readonly<{
type CoderWorkspacesConfig = Readonly<{
mode: 'manual' | 'auto';
params: Record<string, string | undefined>;
creationUrl: string;
repoUrl: string | undefined;
repoUrlParamKeys: [string, ...string[]][];
templateName: string;
Expand All @@ -90,7 +91,7 @@ const appConfig: CoderAppConfig = {
},

workspaces: {
templateName: 'devcontainers',
templateName: 'devcontainers-a',
mode: 'manual',
repoUrlParamKeys: ['custom_repo', 'repo_url'],
params: {
Expand All @@ -112,7 +113,7 @@ spec:
lifecycle: unknown
owner: pms
coder:
templateName: 'devcontainers'
templateName: 'devcontainers-b'
mode: 'auto'
params:
repo: 'custom'
Expand All @@ -122,17 +123,22 @@ spec:
Your output will look like this:

```tsx
const config: CoderEntityConfig = {
const config: CoderWorkspacesConfig = {
mode: 'auto',
params: {
repo: 'custom',
region: 'us-pittsburgh',
custom_repo: 'https://github.com/Parkreiner/python-project/',
repo_url: 'https://github.com/Parkreiner/python-project/',
},
repoUrl: 'https://github.com/Parkreiner/python-project/',
repoUrlParamKeys: ['custom_repo', 'repo_url'],
templateName: 'devcontainers',
repoUrl: 'https://github.com/Parkreiner/python-project/',

// Other URL parameters will be included in real code
// but were stripped out for this example
creationUrl:
'https://dev.coder.com/templates/devcontainers-b/workspace?mode=auto',
};
```

Expand All @@ -146,6 +152,7 @@ const config: CoderEntityConfig = {
3. Go through all properties parsed from `catalog-info.yaml` and inject those. If the properties are already defined, overwrite them
4. Grab the repo URL from the entity's location fields.
5. For each key in `CoderAppConfig`'s `workspaces.repoUrlParamKeys` property, take that key, and inject it as a key-value pair, using the URL as the value. If the key already exists, always override it with the URL
6. Use the Coder access URL and the properties defined during the previous steps to create the URL for creating new workspaces, and then inject that.

## `Workspace`

Expand Down
10 changes: 5 additions & 5 deletions plugins/backstage-plugin-coder/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { parse } from 'valibot';
import { type UseQueryOptions } from '@tanstack/react-query';

import { CoderEntityConfig } from './hooks/useCoderEntityConfig';
import { CoderWorkspacesConfig } from './hooks/useCoderWorkspacesConfig';
import {
type Workspace,
workspaceBuildParametersSchema,
Expand Down Expand Up @@ -144,7 +144,7 @@ async function getWorkspaceBuildParameters(inputs: BuildParamsFetchInputs) {

type WorkspacesByRepoFetchInputs = Readonly<
WorkspacesFetchInputs & {
repoConfig: CoderEntityConfig;
workspacesConfig: CoderWorkspacesConfig;
}
>;

Expand All @@ -162,7 +162,7 @@ export async function getWorkspacesByRepo(
),
);

const { repoConfig } = inputs;
const { workspacesConfig } = inputs;
const matchedWorkspaces: Workspace[] = [];

for (const [index, res] of paramResults.entries()) {
Expand All @@ -172,8 +172,8 @@ export async function getWorkspacesByRepo(

for (const param of res.value) {
const include =
repoConfig.repoUrlParamKeys.includes(param.name) &&
param.value === repoConfig.repoUrl;
workspacesConfig.repoUrlParamKeys.includes(param.name) &&
param.value === workspacesConfig.repoUrl;

if (include) {
// Doing type assertion just in case noUncheckedIndexedAccess compiler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ export const CoderAuthInputForm = () => {
// won't connect the label and input together, which breaks
// accessibility for screen readers. Need to wire up extra IDs, sadly.
label="Auth token"
id={authTokenInputId}
InputLabelProps={{ htmlFor: authTokenInputId }}
InputProps={{ id: authTokenInputId }}
required
name="authToken"
type="password"
Expand Down
Loading