diff --git a/plugins/backstage-plugin-coder/README.md b/plugins/backstage-plugin-coder/README.md index eea90e07..5ddc8519 100644 --- a/plugins/backstage-plugin-coder/README.md +++ b/plugins/backstage-plugin-coder/README.md @@ -89,7 +89,7 @@ the devcontainer. ); ``` - + **Note:** You can also wrap a single page or component with `CoderProvider` if you only need Coder in a specific part of your app. See our [API reference](./docs/README.md) (particularly the section on [the `CoderProvider` component](./docs/components.md#coderprovider)) for more details. 1. Add the `CoderWorkspacesCard` card to the entity page in your app: diff --git a/plugins/backstage-plugin-coder/docs/README.md b/plugins/backstage-plugin-coder/docs/README.md new file mode 100644 index 00000000..7ca73a4e --- /dev/null +++ b/plugins/backstage-plugin-coder/docs/README.md @@ -0,0 +1,9 @@ +# Plugin API Reference – Coder for Backstage + +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). + +## Documentation directory + +- [Components](./components.md) +- [Custom React hooks](./hooks.md) +- [Important types](./types.md) diff --git a/plugins/backstage-plugin-coder/docs/components.md b/plugins/backstage-plugin-coder/docs/components.md new file mode 100644 index 00000000..e37aff20 --- /dev/null +++ b/plugins/backstage-plugin-coder/docs/components.md @@ -0,0 +1,540 @@ +# Plugin API reference – React components + +This is the main documentation page for the Coder plugin's React components. + +## Component list + +- [`CoderAuthWrapper`](#coderauthwrapper) +- [`CoderErrorBoundary`](#codererrorboundary) +- [`CoderProvider`](#coderprovider) +- [`CoderWorkspacesCard`](#coderworkspacescard) + - [`CoderWorkspacesCard.CreateWorkspacesLink`](#coderworkspacescardcreateworkspaceslink) + - [`CoderWorkspacesCard.ExtraActionsButton`](#coderworkspacescardextraactionsbutton) + - [`CoderWorkspacesCard.HeaderRow`](#coderworkspacescardheaderrow) + - [`CoderWorkspacesCard.Root`](#coderworkspacescardroot) + - [`CoderWorkspacesCard.SearchBox`](#coderworkspacescardsearchbox) + - [`CoderWorkspacesCard.WorkspacesList`](#coderworkspacescardworkspaceslist) + - [`CoderWorkspacesCard.WorkspacesListIcon`](#coderworkspacescardworkspaceslisticon) + - [`CoderWorkspacesCard.WorkspacesListItem`](#coderworkspacescardworkspaceslistitem) + +## `CoderAuthWrapper` + +This component is designed to simplify authentication checks for other components that need to be authenticated with Coder. Place any child component inside the wrapper. If the user is authenticated, they will see the children. Otherwise, they will see a form for authenticating themselves. + +### Type signature + +```tsx +type Props = Readonly< + PropsWithChildren<{ + type: 'card'; + }> +>; + +declare function CoderAuthWrapper(props: Props): JSX.Element; +``` + +### Sample usage + +```tsx +function YourComponent() { + // This query requires authentication + const query = useCoderWorkspaces('owner:lil-brudder'); + return

{query.isLoading ? 'Loading' : 'Not loading'}

; +} + + + + + +; +``` + +### Throws + +- Throws a render error if this component mounts outside of `CoderProvider` + +### Notes + +- The wrapper will also stop displaying the child component(s) if the auth token expires, or if the token cannot be safely verified. If that happens, the component will also display some form controls for troubleshooting. +- `CoderAuthWrapper` only supports the `card` type for now, but more types will be added as we add more UI components to the library + +## `CoderErrorBoundary` + +Provides an error boundary for catching render errors thrown by Coder's custom hooks (e.g., parsing logic). + +### Type signature + +```tsx +type Props = { + children?: ReactNode; + fallbackUi?: ReactNode; +}; + +declare function CoderErrorBoundary(props: Props): JSX.Element; +``` + +### Sample usage + +```tsx +function YourComponent() { + // Pretend that there is an issue with this hook, and that it will always + // throw an error + const config = useCoderEntityConfig(); + return

Will never reach this code

; +} + + + +; +``` + +### Throws + +- Does not throw + +### Notes + +- All other Coder components are exported with this component wrapped around them. Unless you are making extension use of the plugin's custom hooks, it is not expected that you will need this component. +- If `fallbackUi` is not specified, `CoderErrorBoundary` will default to a simple error message +- Although Backstage automatically places error boundaries around each exported component, `CoderErrorBoundary` is designed to handle and process specific kinds of errors from the Coder plugins. + +## `CoderProvider` + +Provides top-level Coder-specific data to the rest of the frontend Coder plugin components. Data such as: + +- The Coder access URL +- Fallback workspace parameters + +### Type signature + +```tsx +type Props = PropsWithChildren<{ + children?: React.ReactNode; + appConfig: CoderAppConfig; + queryClient?: QueryClient; +}>; + +declare function CoderProvider(props: Props): JSX.Element; +``` + +The type of `QueryClient` comes from [Tanstack Router v4](https://tanstack.com/query/v4/docs/reference/QueryClient). + +### Sample usage + +```tsx +function YourComponent() { + const query = useCoderWorkspaces('owner:brennan-lee-mulligan'); + return ( + + ); +} + +const appConfig: CoderAppConfig = { + deployment: { + accessUrl: 'https://dev.coder.com', + }, + + workspaces: { + templateName: 'devcontainers', + mode: 'manual', + repoUrlParamKeys: ['custom_repo', 'repo_url'], + params: { + repo: 'custom', + region: 'eu-helsinki', + }, + }, +}; + + + +; +``` + +### Throws + +- Does not throw + +### Notes + +- This component was deliberately designed to be agnostic of as many Backstage APIs as possible - it can be placed as high as the top of the app, or treated as a wrapper around a specific plugin component. + - That said, it is recommended that only have one instance of `CoderProvider` per Backstage deployment. Multiple `CoderProvider` component instances could interfere with each other and accidentally fragment caching state +- If you are already using TanStack Query in your deployment, you can provide your own `QueryClient` value via the `queryClient` prop. + - If not specified, `CoderProvider` will use its own client + - Even if you aren't using TanStack Query anywhere else, you could consider adding your own client to configure it with more specific settings + - All Coder-specific queries use a query key prefixed with `coder-backstage-plugin` to prevent any accidental key collisions. +- Regardless of whether you pass in a custom `queryClient` value, `CoderProvider` will spy on the active client to detect any queries that likely failed because of Coder auth tokens expiring + +## `CoderWorkspacesCard` + +Allows you to search for and display Coder workspaces that the currently-authenticated user has access to. The component handles all data-fetching, caching + +Has two "modes" – one where the component has access to all Coder workspaces for the user, and one where the component is aware of entity data and filters workspaces to those that match the currently-open repo page. See sample usage for examples. + +All "pieces" of the component are also available as modular sub-components that can be imported and composed together individually. + +### Type signature + +```tsx +type Props = { + queryFilter?: string; + defaultQueryFilter?: string; + onFilterChange?: (newFilter: string) => void; + readEntityData?: boolean; + + // Plus all props from the native HTMLDivElement type, except + // "role", "aria-labelledby", and "children" +}; + +declare function CoderWorkspacesCard(props: Props): JSX.Element; +``` + +### Sample usage + +In "general mode" – the component displays ALL user workspaces: + +```tsx +const appConfig: CoderAppConfig = { + /* Content goes here */ +}; + +// If readEntityData is false or not specified, the component +// can effectively be placed anywhere, as long as it's wrapped +// in a provider + + +; +``` + +In "aware mode" – the component only displays workspaces that +match the repo data for the currently-open entity page: + +```tsx +const appConfig: CoderAppConfig = { + /* Content goes here */ +}; + +// While readEntityData is true, it must be placed somewhere +// that exposes entity data via React Context + + + + + + +; +``` + +Using the component as a controlled component: + +```tsx +function YourComponent() { + const [searchText, setSearchText] = useState('owner:me'); + + return ( + setSearchText(newSearchText)} + /> + ); +} + + + +; +``` + +### Throws + +- Will throw a render error if called outside `CoderProvider`. +- Will throw a render error if the value of `readEntityData` changes across re-renders – it must remain a static value for the entire lifecycle of the component. +- If `readEntityData` is `true`: will throw if the component is called outside of an `EntityLayout` (or any other component that exposes entity data via React Context) + +### Notes + +- All `CoderWorkspacesCard` (and its sub-components) have been designed with accessibility in mind: + - All content is accessible via screen reader - all icon buttons have accessible text + - There are no color contrast violations in the components' default color schemes (with either the dark or light themes) + - When wired together properly (`CoderWorkspacesCard` does this automatically), the entire search component is exposed as an accessible search landmark for screen readers +- `queryFilter` and `onFilterChange` allow you to change the component from [being uncontrolled to controlled](https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable). If `queryFilter` is not specified, the component will manage all its search state internally. +- See notes for the individual sub-components for additional information. + +## `CoderWorkspacesCard.CreateWorkspacesLink` + +A link-button for creating new workspaces. Clicking this link will take you to "create workspace page" in your Coder deployment, with as many fields filled out as possible. + +### Type definition + +```tsx +type Props = { + tooltipText?: string; + tooltipProps?: Omit; + tooltipRef?: ForwardedRef; + + // Also supports all props from the native HTMLAnchorElement + // component type +}; + +declare function CreateWorkspacesLink( + props: Props, + ref?: ForwardedRef, +): JSX.Element; +``` + +All Tooltip-based props come from the type definitions for the MUI `Tooltip` component. + +### Throws + +- Will throw a render error if called outside of either a `CoderProvider` or `CoderWorkspacesCard.Root` + +### Notes + +- If `readEntityData` is `true` in `CoderWorkspacesCard.Root`: this component will include YAML properties parsed from the current page's entity data. + +## `CoderWorkspacesCard.ExtraActionsButton` + +A contextual menu of additional tertiary actions that can be performed for workspaces. Current actions: + +- Refresh workspaces list +- Eject token + +### Type definition + +```tsx +type ExtraActionsButtonProps = Omit< + ButtonHTMLAttributes, + 'id' | 'aria-controls' +> & { + onClose?: MenuProps['onClose']; + toolTipProps?: Omit; + tooltipText?: string; + tooltipRef?: ForwardedRef; + + menuProps?: Omit< + MenuProps, + | 'id' + | 'open' + | 'anchorEl' + | 'MenuListProps' + | 'children' + | 'onClose' + | 'getContentAnchorEl' + > & { + MenuListProps: Omit; + }; + + // Also supports all props from the native HTMLButtonElement + // component, except "id" and "aria-controls" +}; + +declare function ExtraActionsButton( + props: Props, + ref?: ForwardedRef, +): JSX.Element; +``` + +All Tooltip- and Menu-based props come from the type definitions for the MUI `Tooltip` and `Menu` components. + +### Throws + +- Will throw a render error if called outside of either a `CoderProvider` or `CoderWorkspacesCard.Root` + +### Notes + +- When the menu opens, the first item of the list will auto-focus +- While the menu is open, you can navigate through items with the Up and Down arrow keys on the keyboard. These instructions are available for screen readers to announce + +## `CoderWorkspacesCard.HeaderRow` + +Provides a wrapper around various heading information, as well as a section for additional buttons/actions to go. Provides critical landmark information for screen readers. + +### Type definition + +```tsx +type HeaderProps = { + headerText?: string; + headerLevel?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; + actions?: ReactNode; + fullBleedLayout?: boolean; + activeRepoFilteringText?: string | ReactNode; + + headerClassName?: string; + hgroupClassName?: string; + subheaderClassName?: string; + + // Also supports all props from the native HTMLDivElement + // component except "children" +}; + +declare function HeaderGroup( + props: Props, + ref?: ForwardedRef, +): JSX.Element; +``` + +### Throws + +- Will throw a render error if called outside of either a `CoderProvider` or `CoderWorkspacesCard.Root` + +### Notes + +- If `headerLevel` is not specified, the component will default to `h2` +- If `fullBleedLayout` is `true`, the component will exert negative horizontal margins to fill out its parent +- If `activeRepoFilteringText` will only display if the value of `readEntityData` in `CoderWorkspacesCard.Root` is `true` + +## `CoderWorkspacesCard.Root` + +Wrapper that acts as a context provider for all other sub-components in `CoderWorkspacesCard` – does not define any components that will render to HTML. + +### Type definition + +```tsx +type WorkspacesCardContext = { + queryFilter: string; + onFilterChange: (newFilter: string) => void; + workspacesQuery: UseQueryResult; + headerId: string; + entityConfig: CoderEntityConfig | undefined; +}; + +declare function Root(props: Props): JSX.Element; +``` + +All props mirror those returned by [`useWorkspacesCardContext`](./hooks.md#useworkspacescardcontext) + +### Throws + +- Will throw a render error if called outside of a `CoderProvider` + +### Notes + +- If `entityConfig` is defined, the Root will auto-filter all workspaces down to those that match the repo for the currently-opened entity page +- The key for `entityConfig` is not optional – even if it isn't defined, it must be explicitly passed an `undefined` value + +## `CoderWorkspacesCard.SearchBox` + +Provides the core search functionality for Coder workspaces. + +### Type definition + +```tsx +type Props = { + searchInputRef?: ForwardedRef; + clearButtonRef?: ForwardedRef; + + labelWrapperClassName?: string; + clearButtonClassName?: string; + searchInputClassName?: string; + + // Also supports all props from the native HTMLFieldSetElement + // component, except "children" and "aria-labelledby" +}; + +declare function SearchBox(props: Props): JSX.Element; +``` + +### Throws + +- Will throw a render error if called outside of either a `CoderProvider` or `CoderWorkspacesCard.Root` + +### Notes + +- The logic for processing user input into a new workspaces query is automatically debounced to wait 400ms. + +## `CoderWorkspacesCard.WorkspacesList` + +Main container for displaying all workspaces returned from a query. + +### Type definition + +```tsx +type RenderListItemInput = Readonly<{ + workspace: Workspace; + index: number; + workspaces: readonly Workspace[]; +}>; + +type Props = { + emptyState?: ReactNode; + ordered?: boolean; + listClassName?: string; + fullBleedLayout?: boolean; + renderListItem?: (input: RenderListItemInput) => ReactNode; + + // Also supports all props from the native HTMLDivElement + // component, except for "children" +}; +``` + +### Throws + +- Will throw a render error if called outside of either a `CoderProvider` or `CoderWorkspacesCard.Root` + +### Notes + +- If `ordered` is `true`, the component will render as an `
    `. Otherwise, the output will be a `
      `. `ordered` defaults to `true`. +- If `fullBleedLayout` is `true`, the component will exert negative horizontal margins to fill out its parent +- If `renderListItem` is not specified, this component will default to rendering each list item with [`CoderWorkspacesCard.ListItem`](./components.md#coderworkspacescardworkspaceslistitem) + +## `CoderWorkspacesCard.WorkspacesListIcon` + +The image to use to represent each workspace. + +### Type definition + +```tsx +type WorkspaceListIconProps = { + src: string; + workspaceName: string; + imageClassName?: string; + imageRef?: ForwardedRef; + + // Also accepts all props from the native HTMLDivElement component, + // except "children" and "aria-hidden" +}; + +declare function WorkspaceListIcon(prop: Props): JSX.Element; +``` + +### Throws + +- Does not throw (even if outside `CoderWorkspacesList.Root`) + +### Notes + +- If there is no `src` available to pass to this component, use an empty string. +- When there is no `src` value, the component will display a fallback graphic + +## `CoderWorkspacesCard.WorkspacesListItem` + +The default render component to use when the `renderListItem` prop for [`CoderWorkspacesCard.WorkspacesList`] is not defined. + +### Type definition + +```tsx +type Props = { + workspace: Workspace; + + buttonClassName?: string; + linkClassName?: string; + listFlexRowClassName?: string; + onlineStatusContainerClassName?: string; + onlineStatusLightClassName?: string; + + // Also supports all props from the native HTMLLIElement + // component, except for "children" +}; + +declare function WorkspaceListItem(props: Props): JSX.Element; +``` + +### Throws + +- Will throw a render error if called outside of either a `CoderProvider` (can be called outside of a `CoderWorkspacesCard.Root`) + +### Notes + +- Supports full link-like functionality (right-clicking and middle-clicking to open in a new tab, etc.) diff --git a/plugins/backstage-plugin-coder/docs/hooks.md b/plugins/backstage-plugin-coder/docs/hooks.md new file mode 100644 index 00000000..0b9865a9 --- /dev/null +++ b/plugins/backstage-plugin-coder/docs/hooks.md @@ -0,0 +1,190 @@ +# Plugin API reference – React hooks + +This is the main documentation page for the Coder plugin's React hooks. + +## Hook list + +- [`useCoderEntityConfig`](#useCoderEntityConfig) +- [`useCoderWorkspaces`](#useCoderWorkspaces) +- [`useWorkspacesCardContext`](#useWorkspacesCardContext) + +## `useCoderEntityConfig` + +This hook gives you access to compiled [`CoderEntityConfig`](./types.md#coderentityconfig) data. + +### Type signature + +```tsx +declare function useCoderEntityConfig(): CoderEntityConfig; +``` + +[Type definition for `CoderEntityConfig`](./types.md#coderentityconfig) + +### Example usage + +```tsx +function YourComponent() { + const config = useCoderEntityConfig(); + return

      Your repo URL is {config.repoUrl}

      ; +} + +// All other components provided via @backstage/plugin-catalog +// and should be statically initialized +const overviewContent = ( + + + + + +); + +const serviceEntityPage = ( + + + {overviewContent} + + +); + +// etc. +``` + +### 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) + +### 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 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` + +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. + +### Type signature + +```ts +type UseCoderWorkspacesOptions = Readonly< + Partial<{ + repoConfig: CoderEntityConfig; + }> +>; + +declare function useCoderEntityConfig( + coderQuery: string, + options?: UseCoderWorkspacesOptions, +): UseQueryResult; +``` + +### Example usage + +```tsx +function YourComponent() { + const entityConfig = useCoderEntityConfig(); + const [filter, setFilter] = useState('owner:me'); + + const query = useCoderWorkspaces(filter, { + repoConfig: entityConfig, + }); + + return ( + <> + {query.isLoading && } + {query.isError && } + + {query.data?.map(workspace => ( +
        +
      1. {workspace.name}
      2. +
      + ))} + + ); +} + +const coderAppConfig: CoderAppConfig = { + // ...Properties go here +}; + + + + + +; +``` + +### Throws + +- Will throw an error if called outside a React component +- Will throw an error if the component calling the hook is not wrapped inside a [`CoderProvider`](./components.md#CoderProvider) + +### Notes + +- `UseQueryResult` is taken from [TanStack Query v4](https://tanstack.com/query/v4/docs/framework/react/reference/useQuery) + - We recommend [TK Dodo's Practical React Query blog series](https://tkdodo.eu/blog/practical-react-query) for how to make the most of its features. (Particularly the article on [React Query status checks](https://tkdodo.eu/blog/status-checks-in-react-query)) +- 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) + +## `useWorkspacesCardContext` + +A helper hook for making it easy to share state between a `CoderWorkspacesCardRoot` and the various sub-components for `CoderWorkspacesCard`, without requiring that they all be direct children. + +### Type signature + +```tsx +type WorkspacesCardContext = Readonly<{ + queryFilter: string; + onFilterChange: (newFilter: string) => void; + workspacesQuery: UseQueryResult; + headerId: string; + entityConfig: CoderEntityConfig | undefined; +}>; + +declare function useWorkspacesCardContext(): WorkspacesCardContext; +``` + +### Example usage + +```tsx +function YourComponent1() { + return ( + + + + ); +} + +function YourComponent2() { + return ( + + + + ); +} + +function YourComponent3() { + const { queryFilter, onFilterChange } = useWorkspacesCardContext(); + + return ( + + ); +} + +; +``` + +### Throws + +- If called outside of `CoderProvider` or `CoderWorkspacesCardRoot` + +### Notes + +- See [`CoderWorkspacesCard`](./components.md#coderworkspacescard) for more information. +- `headerId` is for ensuring that the landmark region for `CoderWorkspacesCard` is linked to a header, so that the landmark is available to screen readers. It should be used exclusively for accessibility purposes. diff --git a/plugins/backstage-plugin-coder/docs/types.md b/plugins/backstage-plugin-coder/docs/types.md new file mode 100644 index 00000000..4a0fa72a --- /dev/null +++ b/plugins/backstage-plugin-coder/docs/types.md @@ -0,0 +1,219 @@ +# Plugin API reference – Important types + +## General notes + +- All type definitions for the Coder plugin are defined as type aliases and not interfaces, to prevent the risk of accidental interface merging. If you need to extend from one of our types, you can do it in one of two ways: + + ```tsx + // Type intersection + type CustomType = CoderEntityConfig & { + customProperty: boolean; + }; + + // Interface extension - new interface must have a different name + interface CustomInterface extends CoderEntityConfig { + customProperty: string; + } + ``` + +## Types directory + +- [`CoderAppConfig`](#coderappconfig) +- [`CoderEntityConfig`](#coderentityconfig) +- [`Workspace`](#workspace) +- [`WorkspaceResponse`](#workspaceresponse) + +## `CoderAppConfig` + +Defines a set of configuration options for integrating Backstage with Coder. Primarily has two main uses: + +1. Defining a centralized source of truth for certain Coder configuration options (such as which workspace parameters should be used for injecting repo URL values) +2. Defining "fallback" workspace parameters when a repository entity either doesn't have a `catalog-info.yaml` file at all, or only specifies a handful of properties. + +### Type definition + +```tsx +type CoderAppConfig = Readonly<{ + workspaces: Readonly<{ + templateName: string; + mode?: 'auto' | 'manual' | undefined; + params?: Record; + repoUrlParamKeys: readonly [string, ...string[]]; + }>; + + deployment: Readonly<{ + accessUrl: string; + }>; +}>; +``` + +### Example usage + +See example for [`CoderProvider`](./components.md#coderprovider) + +### Notes + +- `accessUrl` is the URL pointing at your specific Coder deployment +- `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) + +## `CoderEntityConfig` + +Represents the result of compiling Coder plugin configuration data. All data will be compiled from the following sources: + +1. The [`CoderAppConfig`](#coderappconfig) passed to [`CoderProvider`](./components.md#coderprovider) +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<{ + mode: 'manual' | 'auto'; + params: Record; + repoUrl: string | undefined; + repoUrlParamKeys: [string, ...string[]][]; + templateName: string; +}>; +``` + +### Example usage + +Let's say that you have these inputs: + +```tsx +const appConfig: CoderAppConfig = { + deployment: { + accessUrl: 'https://dev.coder.com', + }, + + workspaces: { + templateName: 'devcontainers', + mode: 'manual', + repoUrlParamKeys: ['custom_repo', 'repo_url'], + params: { + repo: 'custom', + region: 'eu-helsinki', + }, + }, +}; +``` + +```yaml +# https://github.com/Parkreiner/python-project/blob/main/catalog-info.yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: python-project +spec: + type: other + lifecycle: unknown + owner: pms + coder: + templateName: 'devcontainers' + mode: 'auto' + params: + repo: 'custom' + region: 'us-pittsburgh' +``` + +Your output will look like this: + +```tsx +const config: CoderEntityConfig = { + 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', +}; +``` + +### Notes + +- See the notes for [`CoderAppConfig`](#coderappconfig) for additional information on some of the fields. +- The value of the `repoUrl` property is derived from [Backstage's `getEntitySourceLocation`](https://backstage.io/docs/reference/plugin-catalog-react.getentitysourcelocation/), which does not guarantee that a URL will always be defined. +- This is the current order of operations used to reconcile param data between `CoderAppConfig`, `catalog-info.yaml`, and the entity location data: + 1. Start with an empty `Record` value + 2. Populate the record with the data from `CoderAppConfig` + 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 + +## `Workspace` + +Represents a single Coder workspace. + +### Type definition + +The below type definitions are likely to be split up at a later date. They are currently defined together for convenience. + +```tsx +type WorkspaceAgentStatus = + | 'connected' + | 'connecting' + | 'disconnected' + | 'timeout'; + +type WorkspaceAgent = { + id: string; + status: WorkspaceAgentStatus; +}; + +type WorkspaceResource = { + id: string; + agents: WorkspaceAgent[]; +}; + +type WorkspaceStatus = + | 'canceled' + | 'canceling' + | 'deleted' + | 'deleting' + | 'failed' + | 'pending' + | 'running' + | 'starting' + | 'stopped' + | 'stopping'; + +type Workspace = { + name: string; + id: string; + template_icon: string; + owner_name: string; + latest_build: { + id: string; + status: WorkspaceStatus; + resources: WorkspaceResource[]; + }; +}; +``` + +### Notes + +- Right now, the number of fields is limited. One planned feature is to expand the type definition to make all Coder workspace properties available + +## `WorkspaceResponse` + +Represents the JSON value that will be part of the response to any workspace API call. + +### Type definition + +```tsx +type WorkspaceResponse = { + count: number; + workspaces: Workspace[]; +}; +``` + +### Notes + +- `count` is the total number of workspaces in the response diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CreateWorkspaceLink.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CreateWorkspaceLink.tsx index fa88f925..eea5132c 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CreateWorkspaceLink.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CreateWorkspaceLink.tsx @@ -1,8 +1,4 @@ -import React, { - type AnchorHTMLAttributes, - type ForwardedRef, - forwardRef, -} from 'react'; +import React, { type AnchorHTMLAttributes, type ForwardedRef } from 'react'; import { makeStyles } from '@material-ui/core'; import { useCoderAppConfig } from '../CoderProvider'; @@ -43,43 +39,42 @@ type CreateButtonLinkProps = Readonly< } >; -export const CreateWorkspaceLink = forwardRef( - (props: CreateButtonLinkProps, ref?: ForwardedRef) => { - const { - children, - className, - tooltipRef, - target = '_blank', - tooltipText = 'Add a new workspace', - tooltipProps = {}, - ...delegatedProps - } = props; +export const CreateWorkspaceLink = ({ + children, + className, + tooltipRef, + target = '_blank', + tooltipText = 'Add a new workspace', + tooltipProps = {}, + ...delegatedProps +}: CreateButtonLinkProps) => { + const styles = useStyles(); + const appConfig = useCoderAppConfig(); + const { entityConfig } = useWorkspacesCardContext(); - const styles = useStyles(); - const appConfig = useCoderAppConfig(); - const { entityConfig } = useWorkspacesCardContext(); - const activeConfig = entityConfig ?? appConfig.workspaces; + const activeConfig = { + ...appConfig.workspaces, + ...(entityConfig ?? {}), + }; - return ( - - - {children ?? } + return ( + + + {children ?? } - - {tooltipText} - {target === '_blank' && <> (Link opens in new tab)} - - - - ); - }, -); + + {tooltipText} + {target === '_blank' && <> (Link opens in new tab)} + + + + ); +}; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx index 07f6d9db..d9c693b0 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx @@ -1,7 +1,6 @@ import React, { type ButtonHTMLAttributes, type ForwardedRef, - forwardRef, useEffect, useRef, useState, @@ -73,6 +72,7 @@ type ExtraActionsMenuProps = Readonly< type ExtraActionsButtonProps = Readonly< Omit, 'id' | 'aria-controls'> & { onClose?: MenuProps['onClose']; + buttonRef?: ForwardedRef; menuProps?: ExtraActionsMenuProps; toolTipProps?: Omit; tooltipText?: string; @@ -80,22 +80,18 @@ type ExtraActionsButtonProps = Readonly< } >; -export const ExtraActionsButton = forwardRef< - HTMLButtonElement, - ExtraActionsButtonProps ->((props, ref) => { - const { - menuProps, - toolTipProps, - tooltipRef, - children, - className, - onClick: outerOnClick, - onClose: outerOnClose, - tooltipText = 'See additional workspace actions', - ...delegatedButtonProps - } = props; - +export const ExtraActionsButton = ({ + menuProps, + buttonRef, + toolTipProps, + tooltipRef, + children, + className, + onClick: outerOnClick, + onClose: outerOnClose, + tooltipText = 'See additional workspace actions', + ...delegatedButtonProps +}: ExtraActionsButtonProps) => { const { className: menuListClassName, ref: menuListRef, @@ -119,7 +115,7 @@ export const ExtraActionsButton = forwardRef< <>