Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 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
901f34c
fix: delete stale DataReminder file
Parkreiner Mar 23, 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
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
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
1 change: 0 additions & 1 deletion plugins/backstage-plugin-coder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@tanstack/react-query": "4.36.1",
"react-use": "^17.2.4",
"valibot": "^0.28.1"
},
"peerDependencies": {
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 @@ -4,25 +4,22 @@ import React, {
useContext,
} from 'react';

import type { YamlConfig } from '../../hooks/useCoderEntityConfig';

export type CoderWorkspaceConfig = Readonly<
Exclude<YamlConfig, undefined> & {
// Only specified explicitly to make templateName required
templateName: string;

// Defined like this to ensure array always has at least one element
repoUrlParamKeys: readonly [string, ...string[]];
}
>;

export type CoderDeploymentConfig = Readonly<{
accessUrl: string;
}>;
import type { YamlConfig } from '../../hooks/useCoderWorkspacesConfig';

export type CoderAppConfig = Readonly<{
workspaces: CoderWorkspaceConfig;
deployment: CoderDeploymentConfig;
deployment: Readonly<{
accessUrl: string;
}>;

workspaces: Readonly<
Exclude<YamlConfig, undefined> & {
// Only specified explicitly to make templateName required
templateName: string;

// Defined like this to ensure array always has at least one element
repoUrlParamKeys: readonly [string, ...string[]];
}
>;
}>;

const AppConfigContext = createContext<CoderAppConfig | null>(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ export const CreateWorkspaceLink = ({
...delegatedProps
}: CreateButtonLinkProps) => {
const styles = useStyles();
const { workspaceCreationLink } = useWorkspacesCardContext();
const { workspacesConfig } = useWorkspacesCardContext();

return (
<Tooltip ref={tooltipRef} title={tooltipText} {...tooltipProps}>
<a
target={target}
className={`${styles.root} ${className ?? ''}`}
href={workspaceCreationLink}
href={workspacesConfig.creationUrl}
{...delegatedProps}
>
{children ?? <AddIcon />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ export const HeaderRow = ({
fullBleedLayout = true,
...delegatedProps
}: HeaderProps) => {
const { headerId, entityConfig } = useWorkspacesCardContext();
const { headerId, workspacesConfig } = useWorkspacesCardContext();
const styles = useStyles({ fullBleedLayout });

const HeadingComponent = headerLevel ?? 'h2';
const repoUrl = entityConfig?.repoUrl;
const { repoUrl } = workspacesConfig;

return (
<div className={`${styles.root} ${className ?? ''}`} {...delegatedProps}>
Expand Down
Loading