Skip to content

Commit

Permalink
chore(Coder plugin): Create guide for working with the Coder SDK (#133)
Browse files Browse the repository at this point in the history
* chore: add vendored version of experimental Coder SDK

* chore: update CoderClient class to use new SDK

* chore: delete mock SDK

* fix: improve data hiding for CoderSdk

* docs: update typo

* wip: commit progress on updating Coder client

* wip: commit more progress on updating types

* chore: remove valibot type definitions from global constants file

* chore: rename mocks file

* fix: update type mismatches

* wip: commit more update progress

* wip: commit progress on updating client/SDK integration

* fix: get all tests passing for CoderClient

* fix: update UrlSync updates

* fix: get all tests passing

* chore: update all mock data to use Coder core entity mocks

* refactor: improve co-location for useCoderWorkspacesQuery

* wip: commit progress on React Query wrappers

* fix: add extra helpers to useCoderSdk

* wip: add test stubs for useCoderQuery

* fix: add queryKey patching to useCoderQuery

* fix: only add queryKey prefix if it is missing

* fix: make Coder query key prefix an opaque string

* refactor: improve ergonomics of useCoderQuery

* refactor: clean up query key patching logic

* chore: let users disable fallback auth UI

* wip: commit progress on tests

* chore: update wording for clarity

* fix: update import for workspaces card root

* chore: get first test passing

* chore: add inverted promise helper

* fix: make non-authenticated queries fail faster

* fix: update tests to make setup easier

* wip: get another test passing

* chore: finish all initial tests for useCoderQuery

* fix: tighten up types for inverted promises

* fix: more tightening

* fix: make sure queries aren't tried indefinitely by default

* wip: commit docs progress

* fix: increase granularity for auth fallback behavior

* wip: commit more docs progress

* fix: establish better boundaries between hooks

* wip: commit more progress

* wip: more docs progress

* fix: split up auth fallback logic into three providers

* fix: update example code

* wip: commit more progress

* fix: update names for auth fallback modes

* wip: more progress

* fix: remove repetitive wording

* wip: more progress

* fix: add table of contents header

* fix: improve granularity of expired token spy logic

* fix: prevent infinite revalidation loop

* fix: clean up the cleanup logic

* fix: update example code

* fix: update header levels

* fix: make prop optional

* chore: add warning about query client mistakes

* wip: finish last code example

* fix: update union/intersection mismatch

* chore: finish initial version of SDK readme

* wip: make placeholders more obvious

* fix: add additional properties to hide from SDK

* fix: shrink down the API of useCoderSdk

* update method name for clarity

* chore: removal vestigal endpoint properties

* fix: swap public 'SDK' usage with 'API'

* fix: remove temp import

* fix: update exports for end-types

* fix: update query wrapper tests

* wip: commit current rewrite progress

* fix: update structure of directory readme

* wip: commit more docs progress

* chore: finish second draft of main README

* refactor: rename ejectToken to unlinkToken

* refactor: reorganize readme file structure

* update details for new versions of README

* chore: delete first draft of the README

* fix: remove duplicate destructuring

* fix: update duplicate exports

* fix: update semver message

* fix: remove useEffect comparison column

* fix: move custom query client into advanced section

* fix: remove redundant examples

* fix: update hook overview

* fix: update formatting for advanced file

* fix: regorganize prefix section

* chore: finish v3 of reorganization

* chore: reorganize text content one last time

* chore: group prefix examples

* chore: reorganize directory readme

* chore: add image of auth fallback

* chore: add video of auth functionality
  • Loading branch information
Parkreiner committed Jun 17, 2024
1 parent 251214e commit 80d6858
Show file tree
Hide file tree
Showing 30 changed files with 514 additions and 138 deletions.
25 changes: 18 additions & 7 deletions plugins/backstage-plugin-coder/docs/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
# Plugin API Reference – Coder for Backstage
# Documentation Directory – `backstage-plugin-coder` v0.3.0

For users who need more information about how to extend and modify the Coder plugin. For general setup, please see our main [README](../README.md).
This document lists core information for the Backstage Coder plugin. It is intended for users who have already set up the plugin and are looking to take it further.

All documentation reflects version `v0.2.0` of the plugin. Note that breaking API changes may continue to happen for minor versions until the plugin reaches version `v1.0.0`.
For general setup, please see our [main README](../README.md).

## Documentation directory
## Documentation listing

- [Components](./components.md)
- [Custom React hooks](./hooks.md)
- [Important types](./types.md)
### Guides

- [Using the Coder API from Backstage](./guides/coder-api.md)
- [Advanced use cases for the Coder API](./guides//coder-api-advanced.md)

### API reference

- [Components](./api-reference/components.md)
- [Custom React hooks](./api-reference/hooks.md)
- [Important types](./api-reference/types.md)

## Notes about semantic versioning

We fully intend to follow semantic versioning with the Coder plugin for Backstage. Expect some pain points as we figure out the right abstractions needed to hit version 1, but we will try to minimize breaking changes as much as possible as the library gets ironed out.
72 changes: 72 additions & 0 deletions plugins/backstage-plugin-coder/docs/guides/coder-api-advanced.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Working with the Coder API - advanced use cases

This guide covers some more use cases that you can leverage for more advanced configuration of the Coder API from within Backstage.

## Changing fallback auth component behavior

By default, `CoderProvider` is configured to display a fallback auth UI component when two cases are true:

1. The user is not authenticated
2. There are no official Coder components are being rendered to the screen.

<img src="../../screenshots/auth-fallback.png" alt="The Coder auth fallback UI" />

All official Coder plugin components are configured to let the user add auth information if the user isn't already authenticated, so the fallback component only displays when there would be no other way to add the information.

However, depending on your use cases, `CoderProvider` can be configured to change how it displays the fallback, based on the value of the `fallbackAuthUiMode` prop.

```tsx
<CoderProvider fallbackAuthUiMode="assertive">
<OtherComponents />
</CoderProvider>
```

There are three values that can be set for the mode:

- `restrained` (default) - The auth fallback will only display if the user is not authenticated, and there would be no other way for the user to add their auth info.
- `assertive` - The auth fallback will always display when the user is not authenticated, regardless of what Coder component are on-screen. But the fallback will **not** appear if the user is authenticated.
- `hidden` - The auth fallback will never appear under any circumstances. Useful if you want to create entirely custom components and don't mind wiring your auth logic manually via `useCoderAuth`.

## Connecting a custom query client to the Coder plugin

By default, the Coder plugin uses and manages its own query client. This works perfectly well if you aren't using React Query for any other purposes, but if you are using it throughout your Backstage deployment, it can cause issues around redundant state (e.g., not all cached data being vacated when the user logs out).

To prevent this, you will need to do two things:

1. Pass in your custom React Query query client into the `CoderProvider` component
2. "Group" your queries with the Coder query key prefix

```tsx
const yourCustomQueryClient = new QueryClient();

<CoderProvider queryClient={yourCustomQueryClient}>
<YourCustomComponents />
</CoderProvider>;

// Ensure that all queries have the correct query key prefix
import { useQuery } from '@tanstack/react-react-query';
import {
CODER_QUERY_KEY_PREFIX,
useCoderQuery,
} from '@coder/backstage-plugin-coder';

function CustomComponent() {
const query1 = useQuery({
queryKey: [CODER_QUERY_KEY_PREFIX, 'workspaces'],
queryFn: () => {
// Get workspaces here
},
});

// useCoderQuery automatically prefixes all query keys with
// CODER_QUERY_KEY_PREFIX if it's not already the first value of the array
const query2 = useCoderQuery({
queryKey: ['workspaces'],
queryFn: () => {
// Get workspaces here
},
});

return <div>Main component content</div>;
}
```
262 changes: 262 additions & 0 deletions plugins/backstage-plugin-coder/docs/guides/coder-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# Coder API - Quick-start guide

## Overview

The Coder plugin makes it easy to bring the entire Coder API into your Backstage deployment. This guide covers how to get it set up so that you can start accessing Coder from Backstage.

Note: this covers the main expected use cases with the plugin. For more information and options on customizing your Backstage deployment further, see our [Advanced API guide](./coder-api-advanced.md).

### Before you begin

Please ensure that you have the Coder plugin fully installed before proceeding. You can find instructions for getting up and running in [our main README](../../README.md).

### Important hooks for using the Coder API

The Coder plugin exposes three (soon to be four) main hooks for accessing Coder plugin state and making queries/mutations

- `useCoderAuth` - Provides methods and state values for interacting with your current Coder auth session from within Backstage.

```tsx
function SessionTokenInputForm() {
const [sessionTokenDraft, setSessionTokenDraft] = useState('');
const coderAuth = useCoderAuth();

const onSubmit = (event: FormEvent<HTMLFormElement>) => {
coderAuth.registerNewToken(sessionToken);
setSessionTokenDraft('');
};

return (
<form onSubmit={onSubmit}>
<MainFormContent />
</form>
);
}
```

- `useCoderQuery` - Makes it simple to query data from the Coder API and share it throughout your application.

```tsx
function WorkspacesList() {
// Return type matches the return type of React Query's useQuerys
const workspacesQuery = useCoderQuery({
queryKey: ['workspaces'],
queryFn: ({ coderApi }) => coderApi.getWorkspaces({ limit: 5 }),
});
}
```

- `useCoderMutation` (coming soon) - Makes it simple to mutate data via the Coder API.
- `useCoderApi` - Exposes an object with all available Coder API methods. None of the state in this object is tied to React render logic - it can be treated as a "function bucket". Once `useCoderMutation` is available, the main value of this hook will be as an escape hatch in the rare situations where `useCoderQuery` and `useCoderMutation` don't meet your needs. Under the hood, both `useCoderQuery` and `useCoderMutation` receive their `coderApi` context value from this hook.

```tsx
function HealthCheckComponent() {
const coderApi = useCoderApi();
const processWorkspaces = async () => {
const workspacesResponse = await coderApi.getWorkspaces({
limit: 10,
});
processHealthChecks(workspacesResponse.workspaces);
};
}
```

Internally, the Coder plugin uses [React Query/TanStack Query v4](https://tanstack.com/query/v4/docs/framework/react/overview). In fact, `useCoderQuery` and `useCoderMutation` are simply wrappers over `useQuery` and `useMutation`. Both simplify the process of wiring up the hooks' various properties to the Coder auth, while exposing a more convenient way of accessing the Coder API object.

If you ever need to coordinate queries and mutations, you can use `useQueryClient` from React Query - no custom plugin-specific hook needed.

The bottom of this document has examples of both queries and mutations.

### Grouping queries with the Coder query key prefix

The plugin exposes a `CODER_QUERY_KEY_PREFIX` constant that you can use to group all Coder queries. `useCoderQuery` automatically injects this value into all its `queryKey` arrays. However, if you need to escape out with `useQuery`, you can import the constant and manually include it as the first value of your query key.

In addition, all official Coder plugin components use this prefix internally.

```tsx
// All grouped queries can be invalidated at once from the query client
const queryClient = useQueryClient();
const invalidateAllCoderQueries = () => {
queryClient.invalidateQuery({
queryKey: [CODER_QUERY_KEY_PREFIX],
});
};
// The prefix is only needed when NOT using useCoderQuery
const customQuery = useQuery({
queryKey: [CODER_QUERY_KEY_PREFIX, 'workspaces'],
queryFn: () => {
// Your custom API logic
},
});
// When the user unlinks their session token, all queries grouped under
// CODER_QUERY_KEY_PREFIX are vacated from the active query cache
function LogOutButton() {
const { unlinkToken } = useCoderAuth();
return (
<button type="button" onClick={unlinkToken}>
Unlink Coder account
</button>
);
}
```

## Recommendations for accessing the API

1. If querying data, prefer `useCoderQuery`. It automatically wires up all auth logic to React Query (which includes pausing queries if the user is not authenticated). It also lets you access the Coder API via its query function. `useQuery` works as an escape hatch if `useCoderQuery` doesn't meet your needs, but it requires more work to wire up correctly.
2. If mutating data, you will need to call `useMutation`, `useQueryClient`, and `useCoderApi` in tandem\*.

We highly recommend **not** fetching with `useState` + `useEffect`, or with `useAsync`. Both face performance issues when trying to share state. See [ui.dev](https://www.ui.dev/)'s wonderful [_The Story of React Query_ video](https://www.youtube.com/watch?v=OrliU0e09io) for more info on some of the problems they face.

\* `useCoderMutation` can be used instead of all three once that hook is available.

### Comparing query caching strategies

| | `useAsync` | `useQuery` | `useCoderQuery` |
| ------------------------------------------------------------------ | ---------- | ---------- | --------------- |
| Automatically handles race conditions ||||
| Can retain state after component unmounts | 🚫 |||
| Easy, on-command query invalidation | 🚫 | ✅ | ✅ |
| Automatic retry logic when a query fails | 🚫 | ✅ | ✅ |
| Less need to fight dependency arrays | 🚫 | ✅ | ✅ |
| Easy to share state for sibling components | 🚫 | ✅ | ✅ |
| Pre-wired to Coder auth logic | 🚫 | 🚫 | ✅ |
| Can consume Coder API directly from query function | 🚫 | 🚫 | ✅ |
| Automatically groups Coder-related queries by prefixing query keys | 🚫 | 🚫 | ✅ |

## Authentication

All API calls to **any** of the Coder API functions will fail if you have not authenticated yet. Authentication can be handled via any of the official Coder components that can be imported via the plugin. However, if there are no Coder components on the screen, the `CoderProvider` component will automatically\* inject a fallback auth button for letting the user add their auth info.

https://github.com/coder/backstage-plugins/assets/28937484/0ece4410-36fc-4b32-9223-66f35953eeab

Once the user has been authenticated, all Coder API functions will become available. When the user unlinks their auth token (effectively logging out), all cached queries that start with `CODER_QUERY_KEY_PREFIX` will automatically be vacated.

\* This behavior can be disabled. Please see our [advanced API guide](./coder-api-advanced.md) for more information.

## Component examples

Here are some full code examples showcasing patterns you can bring into your own codebase.

Note: To keep the examples simple, none of them contain any CSS styling or MUI components.

### Displaying recent audit logs

```tsx
import React from 'react';
import { useCoderQuery } from '@coder/backstage-plugin-coder';

function RecentAuditLogsList() {
const auditLogsQuery = useCoderQuery({
queryKey: ['audits', 'logs'],
queryFn: ({ coderApi }) => coderApi.getAuditLogs({ limit: 10 }),
});

return (
<>
{auditLogsQuery.isLoading && <p>Loading&hellip;</p>}
{auditLogsQuery.error instanceof Error && (
<p>Encountered the following error: {auditLogsQuery.error.message}</p>
)}

{auditLogsQuery.data !== undefined && (
<ul>
{auditLogsQuery.data.audit_logs.map(log => (
<li key={log.id}>{log.description}</li>
))}
</ul>
)}
</>
);
}
```
## Creating a new workspace
Note: this example showcases how to perform mutations with `useMutation`. The example will be updated once `useCoderMutation` is available.
```tsx
import React, { type FormEvent, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
type CreateWorkspaceRequest,
CODER_QUERY_KEY_PREFIX,
useCoderQuery,
useCoderApi,
} from '@coder/backstage-plugin-coder';

export function WorkspaceCreationForm() {
const [newWorkspaceName, setNewWorkspaceName] = useState('');
const coderApi = useCoderSdk();
const queryClient = useQueryClient();

const currentUserQuery = useCoderQuery({
queryKey: ['currentUser'],
queryFn: coderApi.getAuthenticatedUser,
});

const workspacesQuery = useCoderQuery({
queryKey: ['workspaces'],
queryFn: coderApi.getWorkspaces,
});

const createWorkspaceMutation = useMutation({
mutationFn: (payload: CreateWorkspaceRequest) => {
if (currentUserQuery.data === undefined) {
throw new Error(
'Cannot create workspace without data for current user',
);
}

const { organization_ids, id: userId } = currentUserQuery.data;
return coderApi.createWorkspace(organization_ids[0], userId, payload);
},
});

const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();

// If the mutation fails, useMutation will expose the error in the UI via
// its own exposed properties
await createWorkspaceMutation.mutateAsync({
name: newWorkspaceName,
});

setNewWorkspaceName('');
queryClient.invalidateQueries({
queryKey: [CODER_QUERY_KEY_PREFIX, 'workspaces'],
});
};

return (
<>
{createWorkspaceMutation.isSuccess && (
<p>
Workspace {createWorkspaceMutation.data.name} created successfully!
</p>
)}

<form onSubmit={onSubmit}>
<fieldset>
<legend>Required fields</legend>

<label>
Workspace name
<input
type="text"
value={newWorkspaceName}
onChange={event => setNewWorkspaceName(event.target.value)}
/>
</label>
</fieldset>

<button type="submit">Create workspace</button>
</form>
</>
);
}
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 80d6858

Please sign in to comment.