Skip to content

Commit b19c08e

Browse files
authored
refactor: clean up and consolidate config values in codebase (#29)
* fix: fix hover behavior for last list item * fix: shrink default max height for container * fix: ensure divider bar appears when there is overflow * refactor: add workspaceCreationLink prop to context provider * refactor: split Placeholder into separate component * chore: finish cta button * fix: make sure button only appears when loading is finished * docs: remove bad comment * chore: add explicit return type to useCoderAppConfig for clarity * refactor: consolidate and decouple type definitions * refactor: move dynamic entity config logic * refactor: update references for workspaces config * refactor: centralize creationUrl logic * refactor: rename useCoderEntityConfig to useCoderWorkspacesConfig * refactor: rename old useCoderWorkspaces to useCoderWorkspacesQuery * fix: update typo in test case * fix: update test logic to account for creationUrl * fix: update query logic to account for always-defined workspacesConfig * docs: fix typo in comment * refactor: clean up how mock data is defined * fix: make logic for showing reminder more airtight * refactor: split DataReminder into separate file * refactor: simplify API for useCoderWorkspacesQuery * fix: make sure data reminder only shows when appropriate * fix: delete stale DataReminder file * docs: update type definitions * docs: update hook/type docs to reflect new APIs * docs: fix typo * chore: try removing react-use dependency to make CI happy
1 parent 8f195c5 commit b19c08e

File tree

19 files changed

+394
-372
lines changed

19 files changed

+394
-372
lines changed

plugins/backstage-plugin-coder/docs/components.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ declare function CoderAuthWrapper(props: Props): JSX.Element;
3838
```tsx
3939
function YourComponent() {
4040
// This query requires authentication
41-
const query = useCoderWorkspaces('owner:lil-brudder');
42-
return <p>{query.isLoading ? 'Loading' : 'Not loading'}</p>;
41+
const queryState = useCoderWorkspacesQuery({
42+
coderQuery: 'owner:lil-brudder',
43+
});
44+
45+
return <p>{queryState.isLoading ? 'Loading' : 'Not loading'}</p>;
4346
}
4447

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

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

124127
```tsx
125128
function YourComponent() {
126-
const query = useCoderWorkspaces('owner:brennan-lee-mulligan');
129+
const queryState = useCoderWorkspacesQuery({
130+
coderQuery: 'owner:brennan-lee-mulligan',
131+
});
132+
127133
return (
128134
<ul>
129-
{query.data?.map(workspace => (
135+
{queryState.data?.map(workspace => (
130136
<li key={workspace.id}>{workspace.owner_name}</li>
131137
))}
132138
</ul>
@@ -396,8 +402,8 @@ type WorkspacesCardContext = {
396402
queryFilter: string;
397403
onFilterChange: (newFilter: string) => void;
398404
workspacesQuery: UseQueryResult<readonly Workspace[]>;
405+
workspacesConfig: CoderWorkspacesConfig;
399406
headerId: string;
400-
entityConfig: CoderEntityConfig | undefined;
401407
};
402408

403409
declare function Root(props: Props): JSX.Element;

plugins/backstage-plugin-coder/docs/hooks.md

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,33 @@ This is the main documentation page for the Coder plugin's React hooks.
44

55
## Hook list
66

7-
- [`useCoderEntityConfig`](#useCoderEntityConfig)
8-
- [`useCoderWorkspaces`](#useCoderWorkspaces)
7+
- [`useCoderWorkspacesConfig`](#useCoderWorkspacesConfig)
8+
- [`useCoderWorkspacesQuery`](#useCoderWorkspacesquery)
99
- [`useWorkspacesCardContext`](#useWorkspacesCardContext)
1010

11-
## `useCoderEntityConfig`
11+
## `useCoderWorkspacesConfig`
1212

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

1515
### Type signature
1616

1717
```tsx
18-
declare function useCoderEntityConfig(): CoderEntityConfig;
18+
type UseCoderWorkspacesConfigOptions = Readonly<{
19+
readEntityData?: boolean;
20+
}>;
21+
22+
declare function useCoderWorkspacesConfig(
23+
options: UseCoderWorkspacesConfigOptions,
24+
): CoderWorkspacesConfig;
1925
```
2026

21-
[Type definition for `CoderEntityConfig`](./types.md#coderentityconfig)
27+
[Type definition for `CoderWorkspacesConfig`](./types.md#coderWorkspacesconfig)
2228

2329
### Example usage
2430

2531
```tsx
2632
function YourComponent() {
27-
const config = useCoderEntityConfig();
33+
const config = useCoderWorkspacesConfig();
2834
return <p>Your repo URL is {config.repoUrl}</p>;
2935
}
3036

@@ -52,50 +58,46 @@ const serviceEntityPage = (
5258
### Throws
5359

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

5763
### Notes
5864

59-
- 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
65+
- 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
66+
- 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.
6067
- 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`)
6168

62-
## `useCoderWorkspaces`
69+
## `useCoderWorkspacesQuery`
6370

6471
This hook gives you access to all workspaces that match a given query string. If
65-
[`repoConfig`](#usecoderentityconfig) is defined via `options`, the workspaces returned will be filtered down further to only those that match the the repo.
72+
[`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.
6673

6774
### Type signature
6875

6976
```ts
70-
type UseCoderWorkspacesOptions = Readonly<
71-
Partial<{
72-
repoConfig: CoderEntityConfig;
73-
}>
74-
>;
75-
76-
declare function useCoderEntityConfig(
77-
coderQuery: string,
78-
options?: UseCoderWorkspacesOptions,
77+
type UseCoderWorkspacesQueryOptions = Readonly<{
78+
coderQuery: string;
79+
workspacesConfig?: CoderWorkspacesConfig;
80+
}>;
81+
82+
declare function useCoderWorkspacesConfig(
83+
options: UseCoderWorkspacesQueryOptions,
7984
): UseQueryResult<readonly Workspace[]>;
8085
```
8186

8287
### Example usage
8388

8489
```tsx
8590
function YourComponent() {
86-
const entityConfig = useCoderEntityConfig();
8791
const [filter, setFilter] = useState('owner:me');
88-
89-
const query = useCoderWorkspaces(filter, {
90-
repoConfig: entityConfig,
91-
});
92+
const workspacesConfig = useCoderWorkspacesConfig({ readEntityData: true });
93+
const queryState = useCoderWorkspacesQuery({ filter, workspacesConfig });
9294

9395
return (
9496
<>
95-
{query.isLoading && <YourLoadingIndicator />}
96-
{query.isError && <YourErrorDisplay />}
97+
{queryState.isLoading && <YourLoadingIndicator />}
98+
{queryState.isError && <YourErrorDisplay />}
9799

98-
{query.data?.map(workspace => (
100+
{queryState.data?.map(workspace => (
99101
<ol>
100102
<li key={workspace.key}>{workspace.name}</li>
101103
</ol>
@@ -127,7 +129,8 @@ const coderAppConfig: CoderAppConfig = {
127129
- The underlying query will not be enabled if:
128130
1. The user is not currently authenticated (We recommend wrapping your component inside [`CoderAuthWrapper`](./components.md#coderauthwrapper) to make these checks easier)
129131
2. If `repoConfig` is passed in via `options`: when the value of `coderQuery` is an empty string
130-
- `CoderEntityConfig` is the return type of [`useCoderEntityConfig`](#usecoderentityconfig)
132+
- The `workspacesConfig` property is the return type of [`useCoderWorkspacesConfig`](#usecoderworkspacesconfig)
133+
- 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).
131134

132135
## `useWorkspacesCardContext`
133136

@@ -140,8 +143,8 @@ type WorkspacesCardContext = Readonly<{
140143
queryFilter: string;
141144
onFilterChange: (newFilter: string) => void;
142145
workspacesQuery: UseQueryResult<readonly Workspace[]>;
146+
workspacesConfig: CoderWorkspacesConfig;
143147
headerId: string;
144-
entityConfig: CoderEntityConfig | undefined;
145148
}>;
146149

147150
declare function useWorkspacesCardContext(): WorkspacesCardContext;

plugins/backstage-plugin-coder/docs/types.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@
66

77
```tsx
88
// Type intersection
9-
type CustomType = CoderEntityConfig & {
9+
type CustomType = CoderWorkspacesConfig & {
1010
customProperty: boolean;
1111
};
1212

1313
// Interface extension - new interface must have a different name
14-
interface CustomInterface extends CoderEntityConfig {
14+
interface CustomInterface extends CoderWorkspacesConfig {
1515
customProperty: string;
1616
}
1717
```
1818

1919
## Types directory
2020

2121
- [`CoderAppConfig`](#coderappconfig)
22-
- [`CoderEntityConfig`](#coderentityconfig)
22+
- [`CoderWorkspacesConfig`](#coderworkspacesconfig)
2323
- [`Workspace`](#workspace)
2424
- [`WorkspaceResponse`](#workspaceresponse)
2525

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

62-
## `CoderEntityConfig`
62+
## `CoderWorkspacesConfig`
6363

64-
Represents the result of compiling Coder plugin configuration data. All data will be compiled from the following sources:
64+
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:
6565

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

7070
### Type definition
7171

7272
```tsx
73-
type CoderEntityConfig = Readonly<{
73+
type CoderWorkspacesConfig = Readonly<{
7474
mode: 'manual' | 'auto';
7575
params: Record<string, string | undefined>;
76+
creationUrl: string;
7677
repoUrl: string | undefined;
7778
repoUrlParamKeys: [string, ...string[]][];
7879
templateName: string;
@@ -90,7 +91,7 @@ const appConfig: CoderAppConfig = {
9091
},
9192

9293
workspaces: {
93-
templateName: 'devcontainers',
94+
templateName: 'devcontainers-a',
9495
mode: 'manual',
9596
repoUrlParamKeys: ['custom_repo', 'repo_url'],
9697
params: {
@@ -112,7 +113,7 @@ spec:
112113
lifecycle: unknown
113114
owner: pms
114115
coder:
115-
templateName: 'devcontainers'
116+
templateName: 'devcontainers-b'
116117
mode: 'auto'
117118
params:
118119
repo: 'custom'
@@ -122,17 +123,22 @@ spec:
122123
Your output will look like this:
123124
124125
```tsx
125-
const config: CoderEntityConfig = {
126+
const config: CoderWorkspacesConfig = {
126127
mode: 'auto',
127128
params: {
128129
repo: 'custom',
129130
region: 'us-pittsburgh',
130131
custom_repo: 'https://github.com/Parkreiner/python-project/',
131132
repo_url: 'https://github.com/Parkreiner/python-project/',
132133
},
133-
repoUrl: 'https://github.com/Parkreiner/python-project/',
134134
repoUrlParamKeys: ['custom_repo', 'repo_url'],
135135
templateName: 'devcontainers',
136+
repoUrl: 'https://github.com/Parkreiner/python-project/',
137+
138+
// Other URL parameters will be included in real code
139+
// but were stripped out for this example
140+
creationUrl:
141+
'https://dev.coder.com/templates/devcontainers-b/workspace?mode=auto',
136142
};
137143
```
138144

@@ -146,6 +152,7 @@ const config: CoderEntityConfig = {
146152
3. Go through all properties parsed from `catalog-info.yaml` and inject those. If the properties are already defined, overwrite them
147153
4. Grab the repo URL from the entity's location fields.
148154
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
155+
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.
149156

150157
## `Workspace`
151158

plugins/backstage-plugin-coder/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
"@material-ui/icons": "^4.9.1",
4242
"@material-ui/lab": "4.0.0-alpha.61",
4343
"@tanstack/react-query": "4.36.1",
44-
"react-use": "^17.2.4",
4544
"valibot": "^0.28.1"
4645
},
4746
"peerDependencies": {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { parse } from 'valibot';
22
import { type UseQueryOptions } from '@tanstack/react-query';
33

4-
import { CoderEntityConfig } from './hooks/useCoderEntityConfig';
4+
import { CoderWorkspacesConfig } from './hooks/useCoderWorkspacesConfig';
55
import {
66
type Workspace,
77
workspaceBuildParametersSchema,
@@ -144,7 +144,7 @@ async function getWorkspaceBuildParameters(inputs: BuildParamsFetchInputs) {
144144

145145
type WorkspacesByRepoFetchInputs = Readonly<
146146
WorkspacesFetchInputs & {
147-
repoConfig: CoderEntityConfig;
147+
workspacesConfig: CoderWorkspacesConfig;
148148
}
149149
>;
150150

@@ -162,7 +162,7 @@ export async function getWorkspacesByRepo(
162162
),
163163
);
164164

165-
const { repoConfig } = inputs;
165+
const { workspacesConfig } = inputs;
166166
const matchedWorkspaces: Workspace[] = [];
167167

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

173173
for (const param of res.value) {
174174
const include =
175-
repoConfig.repoUrlParamKeys.includes(param.name) &&
176-
param.value === repoConfig.repoUrl;
175+
workspacesConfig.repoUrlParamKeys.includes(param.name) &&
176+
param.value === workspacesConfig.repoUrl;
177177

178178
if (include) {
179179
// Doing type assertion just in case noUncheckedIndexedAccess compiler

plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAppConfigProvider.tsx

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,22 @@ import React, {
44
useContext,
55
} from 'react';
66

7-
import type { YamlConfig } from '../../hooks/useCoderEntityConfig';
8-
9-
export type CoderWorkspaceConfig = Readonly<
10-
Exclude<YamlConfig, undefined> & {
11-
// Only specified explicitly to make templateName required
12-
templateName: string;
13-
14-
// Defined like this to ensure array always has at least one element
15-
repoUrlParamKeys: readonly [string, ...string[]];
16-
}
17-
>;
18-
19-
export type CoderDeploymentConfig = Readonly<{
20-
accessUrl: string;
21-
}>;
7+
import type { YamlConfig } from '../../hooks/useCoderWorkspacesConfig';
228

239
export type CoderAppConfig = Readonly<{
24-
workspaces: CoderWorkspaceConfig;
25-
deployment: CoderDeploymentConfig;
10+
deployment: Readonly<{
11+
accessUrl: string;
12+
}>;
13+
14+
workspaces: Readonly<
15+
Exclude<YamlConfig, undefined> & {
16+
// Only specified explicitly to make templateName required
17+
templateName: string;
18+
19+
// Defined like this to ensure array always has at least one element
20+
repoUrlParamKeys: readonly [string, ...string[]];
21+
}
22+
>;
2623
}>;
2724

2825
const AppConfigContext = createContext<CoderAppConfig | null>(null);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,14 @@ export const CreateWorkspaceLink = ({
4646
...delegatedProps
4747
}: CreateButtonLinkProps) => {
4848
const styles = useStyles();
49-
const { workspaceCreationLink } = useWorkspacesCardContext();
49+
const { workspacesConfig } = useWorkspacesCardContext();
5050

5151
return (
5252
<Tooltip ref={tooltipRef} title={tooltipText} {...tooltipProps}>
5353
<a
5454
target={target}
5555
className={`${styles.root} ${className ?? ''}`}
56-
href={workspaceCreationLink}
56+
href={workspacesConfig.creationUrl}
5757
{...delegatedProps}
5858
>
5959
{children ?? <AddIcon />}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ export const HeaderRow = ({
7070
fullBleedLayout = true,
7171
...delegatedProps
7272
}: HeaderProps) => {
73-
const { headerId, entityConfig } = useWorkspacesCardContext();
73+
const { headerId, workspacesConfig } = useWorkspacesCardContext();
7474
const styles = useStyles({ fullBleedLayout });
7575

7676
const HeadingComponent = headerLevel ?? 'h2';
77-
const repoUrl = entityConfig?.repoUrl;
77+
const { repoUrl } = workspacesConfig;
7878

7979
return (
8080
<div className={`${styles.root} ${className ?? ''}`} {...delegatedProps}>

0 commit comments

Comments
 (0)