From c116ebc726fecae79ad8550f189c1d9ff681bb38 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 24 May 2024 09:32:36 -0400 Subject: [PATCH 1/9] feat: add auth fallback logic for when official Coder components are not mounted (#128) * wip: commit progress on fallback UI * chore: move dep to peer dependencies * wip: commit more progress * wip: more progress * refactor: consolidate card logic * fix: update component tracking hooks * fix: add a11y landmark to auth fallback * wip: commit more style progress * wip: commit more progress * wip: more progress * wip: cleanup current approach * wip: commit progress on observer approach * wip: fix infinite loop for mutation logic * fix: prevent padding patches from firing too often * fix: improve scoping of style overrides * chore: finish intial version of fallback stylling * fix: tidy up types * wip: create initial version of dialog form * wip: commit progress on modal * chore: finish styling for modal wrapper * fix: update padding for FormDialog * wip: start extracting out auth form * fix: add missing barrel export file * fix: make sure that auth form isn't dismissed early * fix: update auth imports * fix: update spacing for auth modal * refactor: clean up auth provider for clarity * docs: rewrite comment for clarity * fix: improve granularity between official Coder components and user components * fix: update all internal consumers of useCoderAuth * wip: commit initial version of useCoderQuery helper hook * refactor: rename hooks to avoid confusion * fix: update exports for plugin * docs: fill in incomplete sentence * wip: commit initial version of useMutation wrapper * refactor: extract retry factor into global constant * fix: add explicit return type to useCoderMutation * wip: start extracting auth logic into better reusable components * fix: update card to have better styling for body * wip: commit progress on style refactoring * fix: update vertical padding for card wrapper * chore: delete CoderAuthWrapper component * fix: update styling for auth fallback * chore: shrink size of PR * fix: update imports * docs: add comment about description setup * fix: remove risk of runtime render errors in auth form * fix: update imports * fix: update font sizes to use relative units * fix: update peer dependencies for react-dom * refactor: clean up auth revalidation logic * wip: start updating tests for new code changes * fix: adding missing test case for auth card * wip: commit progress on auth form test updates * fix: removal vetigal properties * fix: get all CoderAuthForm tests passing * fix: update import for auth hook in test --- plugins/backstage-plugin-coder/package.json | 3 +- .../components/A11yInfoCard/A11yInfoCard.tsx | 63 +++ .../src/components/A11yInfoCard/index.ts | 1 + .../src/components/Card/Card.tsx | 27 -- .../src/components/Card/index.ts | 1 - .../CoderAuthDistrustedForm.tsx | 18 +- .../CoderAuthForm.test.tsx} | 102 +--- .../CoderAuthForm.tsx} | 68 +-- .../CoderAuthInputForm.tsx | 4 +- .../CoderAuthLoadingState.tsx | 0 .../CoderAuthForm/CoderAuthSuccessStatus.tsx | 61 +++ .../CoderAuthForm/UnlinkAccountButton.tsx | 42 ++ .../src/components/CoderAuthForm/index.ts | 1 + .../CoderAuthFormCardWrapper.test.tsx | 107 +++++ .../CoderAuthFormCardWrapper.tsx | 46 ++ .../CoderAuthFormCardWrapper/index.ts | 1 + .../CoderAuthFormDialog.tsx | 145 ++++++ .../components/CoderAuthFormDialog/index.ts | 1 + .../src/components/CoderAuthWrapper/index.ts | 1 - .../CoderProvider/CoderAuthProvider.tsx | 436 +++++++++++++++--- .../CoderProvider/CoderProvider.test.tsx | 6 +- .../CoderWorkspacesCard.tsx | 25 +- .../ExtraActionsButton.tsx | 4 +- .../CoderWorkspacesCard/HeaderRow.tsx | 32 +- .../components/CoderWorkspacesCard/Root.tsx | 78 ++-- .../src/hooks/useCoderWorkspacesQuery.ts | 4 +- plugins/backstage-plugin-coder/src/plugin.ts | 21 +- .../src/testHelpers/mockBackstageData.ts | 2 - .../src/testHelpers/setup.tsx | 16 +- .../src/typesConstants.ts | 8 + yarn.lock | 61 +-- 31 files changed, 1002 insertions(+), 383 deletions(-) create mode 100644 plugins/backstage-plugin-coder/src/components/A11yInfoCard/A11yInfoCard.tsx create mode 100644 plugins/backstage-plugin-coder/src/components/A11yInfoCard/index.ts delete mode 100644 plugins/backstage-plugin-coder/src/components/Card/Card.tsx delete mode 100644 plugins/backstage-plugin-coder/src/components/Card/index.ts rename plugins/backstage-plugin-coder/src/components/{CoderAuthWrapper => CoderAuthForm}/CoderAuthDistrustedForm.tsx (69%) rename plugins/backstage-plugin-coder/src/components/{CoderAuthWrapper/CoderAuthWrapper.test.tsx => CoderAuthForm/CoderAuthForm.test.tsx} (56%) rename plugins/backstage-plugin-coder/src/components/{CoderAuthWrapper/CoderAuthWrapper.tsx => CoderAuthForm/CoderAuthForm.tsx} (53%) rename plugins/backstage-plugin-coder/src/components/{CoderAuthWrapper => CoderAuthForm}/CoderAuthInputForm.tsx (98%) rename plugins/backstage-plugin-coder/src/components/{CoderAuthWrapper => CoderAuthForm}/CoderAuthLoadingState.tsx (100%) create mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthSuccessStatus.tsx create mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx create mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthForm/index.ts create mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/CoderAuthFormCardWrapper.test.tsx create mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/CoderAuthFormCardWrapper.tsx create mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/index.ts create mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthFormDialog/CoderAuthFormDialog.tsx create mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthFormDialog/index.ts delete mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/index.ts diff --git a/plugins/backstage-plugin-coder/package.json b/plugins/backstage-plugin-coder/package.json index 6dcc24a8..e21caf74 100644 --- a/plugins/backstage-plugin-coder/package.json +++ b/plugins/backstage-plugin-coder/package.json @@ -46,7 +46,8 @@ "valibot": "^0.28.1" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0" }, "devDependencies": { "@backstage/cli": "^0.25.1", diff --git a/plugins/backstage-plugin-coder/src/components/A11yInfoCard/A11yInfoCard.tsx b/plugins/backstage-plugin-coder/src/components/A11yInfoCard/A11yInfoCard.tsx new file mode 100644 index 00000000..4c5959b9 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/A11yInfoCard/A11yInfoCard.tsx @@ -0,0 +1,63 @@ +/** + * @file A slightly different take on Backstage's official InfoCard component, + * with better support for accessibility. + * + * Does not support all of InfoCard's properties just yet. + */ +import React, { type HTMLAttributes, type ReactNode, forwardRef } from 'react'; +import { makeStyles } from '@material-ui/core'; + +export type A11yInfoCardProps = Readonly< + HTMLAttributes & { + headerContent?: ReactNode; + } +>; + +const useStyles = makeStyles(theme => ({ + root: { + color: theme.palette.type, + backgroundColor: theme.palette.background.paper, + padding: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + boxShadow: theme.shadows[1], + }, + + headerContent: { + // Ideally wouldn't be using hard-coded font sizes, but couldn't figure out + // how to use the theme.typography property, especially since not all + // sub-properties have font sizes defined + fontSize: '1.5rem', + color: theme.palette.text.primary, + fontWeight: 700, + borderBottom: `1px solid ${theme.palette.divider}`, + + // Margins and padding are a bit wonky to support full-bleed layouts + marginLeft: `-${theme.spacing(2)}px`, + marginRight: `-${theme.spacing(2)}px`, + padding: `0 ${theme.spacing(2)}px ${theme.spacing(2)}px`, + }, +})); + +// Card should be treated as equivalent to Backstage's official InfoCard +// component; had to make custom version so that it could forward properties for +// accessibility/screen reader support +export const A11yInfoCard = forwardRef( + (props, ref) => { + const { className, children, headerContent, ...delegatedProps } = props; + const styles = useStyles(); + + return ( +
+ {headerContent !== undefined && ( +
{headerContent}
+ )} + + {children} +
+ ); + }, +); diff --git a/plugins/backstage-plugin-coder/src/components/A11yInfoCard/index.ts b/plugins/backstage-plugin-coder/src/components/A11yInfoCard/index.ts new file mode 100644 index 00000000..5ef69f03 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/A11yInfoCard/index.ts @@ -0,0 +1 @@ +export * from './A11yInfoCard'; diff --git a/plugins/backstage-plugin-coder/src/components/Card/Card.tsx b/plugins/backstage-plugin-coder/src/components/Card/Card.tsx deleted file mode 100644 index 995b8e5c..00000000 --- a/plugins/backstage-plugin-coder/src/components/Card/Card.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { type HTMLAttributes, forwardRef } from 'react'; -import { makeStyles } from '@material-ui/core'; - -const useStyles = makeStyles(theme => ({ - root: { - color: theme.palette.type, - backgroundColor: theme.palette.background.paper, - padding: theme.spacing(2), - borderRadius: theme.shape.borderRadius, - boxShadow: theme.shadows[1], - }, -})); - -type CardProps = HTMLAttributes; - -export const Card = forwardRef((props, ref) => { - const { className, ...delegatedProps } = props; - const styles = useStyles(); - - return ( -
- ); -}); diff --git a/plugins/backstage-plugin-coder/src/components/Card/index.ts b/plugins/backstage-plugin-coder/src/components/Card/index.ts deleted file mode 100644 index ca0b0604..00000000 --- a/plugins/backstage-plugin-coder/src/components/Card/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Card'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthDistrustedForm.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthDistrustedForm.tsx similarity index 69% rename from plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthDistrustedForm.tsx rename to plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthDistrustedForm.tsx index 1a63a24a..a37c1916 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthDistrustedForm.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthDistrustedForm.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { CoderLogo } from '../CoderLogo'; -import { LinkButton } from '@backstage/core-components'; import { makeStyles } from '@material-ui/core'; -import { useCoderAuth } from '../CoderProvider'; +import { UnlinkAccountButton } from './UnlinkAccountButton'; const useStyles = makeStyles(theme => ({ root: { @@ -31,8 +30,6 @@ const useStyles = makeStyles(theme => ({ export const CoderAuthDistrustedForm = () => { const styles = useStyles(); - const { ejectToken } = useCoderAuth(); - return (
@@ -43,18 +40,7 @@ export const CoderAuthDistrustedForm = () => {

- - Eject token - +
); }; diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.test.tsx similarity index 56% rename from plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.test.tsx rename to plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.test.tsx index 43199c04..95ce2993 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.test.tsx @@ -8,18 +8,14 @@ import { mockAuthStates, mockCoderAuthToken, } from '../../testHelpers/mockBackstageData'; -import { CoderAuthWrapper } from './CoderAuthWrapper'; +import { CoderAuthForm } from './CoderAuthForm'; import { renderInTestApp } from '@backstage/test-utils'; type RenderInputs = Readonly<{ authStatus: CoderAuthStatus; - childButtonText?: string; }>; -async function renderAuthWrapper({ - authStatus, - childButtonText = 'Default button text', -}: RenderInputs) { +async function renderAuthWrapper({ authStatus }: RenderInputs) { const ejectToken = jest.fn(); const registerNewToken = jest.fn(); @@ -40,50 +36,24 @@ async function renderAuthWrapper({ */ const renderOutput = await renderInTestApp( - - - + , ); return { ...renderOutput, ejectToken, registerNewToken }; } -describe(`${CoderAuthWrapper.name}`, () => { - describe('Displaying main content', () => { - it('Displays the main children when the user is authenticated', async () => { - const buttonText = 'I have secret Coder content!'; - renderAuthWrapper({ - authStatus: 'authenticated', - childButtonText: buttonText, - }); - - const button = await screen.findByRole('button', { name: buttonText }); - - // This assertion isn't necessary because findByRole will throw an error - // if the button can't be found within the expected period of time. Doing - // this purely to make the Backstage linter happy - expect(button).toBeInTheDocument(); - }); - }); - +describe(`${CoderAuthForm.name}`, () => { describe('Loading UI', () => { it('Is displayed while the auth is initializing', async () => { - const buttonText = "You shouldn't be able to see me!"; - renderAuthWrapper({ - authStatus: 'initializing', - childButtonText: buttonText, - }); - - await screen.findByText(/Loading/); - const button = screen.queryByRole('button', { name: buttonText }); - expect(button).not.toBeInTheDocument(); + renderAuthWrapper({ authStatus: 'initializing' }); + const loadingIndicator = await screen.findByText(/Loading/); + expect(loadingIndicator).toBeInTheDocument(); }); }); describe('Token distrusted form', () => { it("Is displayed when the user's auth status cannot be verified", async () => { - const buttonText = 'Not sure if you should be able to see me'; const distrustedTextMatcher = /Unable to verify token authenticity/; const distrustedStatuses: readonly CoderAuthStatus[] = [ 'distrusted', @@ -91,16 +61,11 @@ describe(`${CoderAuthWrapper.name}`, () => { 'deploymentUnavailable', ]; - for (const status of distrustedStatuses) { - const { unmount } = await renderAuthWrapper({ - authStatus: status, - childButtonText: buttonText, - }); - - await screen.findByText(distrustedTextMatcher); - const button = screen.queryByRole('button', { name: buttonText }); - expect(button).not.toBeInTheDocument(); + for (const authStatus of distrustedStatuses) { + const { unmount } = await renderAuthWrapper({ authStatus }); + const message = await screen.findByText(distrustedTextMatcher); + expect(message).toBeInTheDocument(); unmount(); } }); @@ -112,58 +77,28 @@ describe(`${CoderAuthWrapper.name}`, () => { const user = userEvent.setup(); const ejectButton = await screen.findByRole('button', { - name: 'Eject token', + name: /Unlink Coder account/, }); await user.click(ejectButton); expect(ejectToken).toHaveBeenCalled(); }); - - it('Will appear if auth status changes during re-renders', async () => { - const buttonText = "Now you see me, now you don't"; - const { rerender } = await renderAuthWrapper({ - authStatus: 'authenticated', - childButtonText: buttonText, - }); - - // Capture button after it first appears on the screen - const button = await screen.findByRole('button', { name: buttonText }); - - rerender( - - - - - , - ); - - // Assert that the button is now gone - expect(button).not.toBeInTheDocument(); - }); }); describe('Token submission form', () => { it("Is displayed when the token either doesn't exist or is definitely not valid", async () => { - const buttonText = "You're not allowed to gaze upon my visage"; - const tokenFormMatcher = /Please enter a new token/; const statusesForInvalidUser: readonly CoderAuthStatus[] = [ 'invalid', 'tokenMissing', ]; - for (const status of statusesForInvalidUser) { - const { unmount } = await renderAuthWrapper({ - authStatus: status, - childButtonText: buttonText, + for (const authStatus of statusesForInvalidUser) { + const { unmount } = await renderAuthWrapper({ authStatus }); + const form = screen.getByRole('form', { + name: /Authenticate with Coder/, }); - await screen.findByText(tokenFormMatcher); - const button = screen.queryByRole('button', { name: buttonText }); - expect(button).not.toBeInTheDocument(); - + expect(form).toBeInTheDocument(); unmount(); } }); @@ -178,7 +113,8 @@ describe(`${CoderAuthWrapper.name}`, () => { * 1. The auth input is of type password, which does not have a role * compatible with Testing Library; can't use getByRole to select it * 2. MUI adds a star to its labels that are required, meaning that any - * attempts at trying to match the string "Auth token" will fail + * attempts at trying to match string literal "Auth token" will fail; + * have to use a regex selector */ const inputField = screen.getByLabelText(/Auth token/); const submitButton = screen.getByRole('button', { name: 'Authenticate' }); diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.tsx similarity index 53% rename from plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx rename to plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.tsx index b0e6ee22..638a1a75 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.tsx @@ -1,52 +1,30 @@ -import React, { type FC, type PropsWithChildren } from 'react'; -import { useCoderAuth } from '../CoderProvider'; -import { InfoCard } from '@backstage/core-components'; +import React from 'react'; +import { useInternalCoderAuth } from '../CoderProvider'; import { CoderAuthDistrustedForm } from './CoderAuthDistrustedForm'; -import { makeStyles } from '@material-ui/core'; import { CoderAuthLoadingState } from './CoderAuthLoadingState'; import { CoderAuthInputForm } from './CoderAuthInputForm'; +import { CoderAuthSuccessStatus } from './CoderAuthSuccessStatus'; -const useStyles = makeStyles(theme => ({ - cardContent: { - paddingTop: theme.spacing(5), - paddingBottom: theme.spacing(5), - }, -})); +export type CoderAuthFormProps = Readonly<{ + descriptionId?: string; +}>; -function CoderAuthCard({ children }: PropsWithChildren) { - const styles = useStyles(); - return ( - -
{children}
-
- ); -} - -type WrapperProps = Readonly< - PropsWithChildren<{ - type: 'card'; - }> ->; - -export const CoderAuthWrapper = ({ children, type }: WrapperProps) => { - const auth = useCoderAuth(); - if (auth.isAuthenticated) { - return <>{children}; - } - - let Wrapper: FC>; - switch (type) { - case 'card': { - Wrapper = CoderAuthCard; - break; - } - default: { - assertExhaustion(type); - } - } +export const CoderAuthForm = ({ descriptionId }: CoderAuthFormProps) => { + const auth = useInternalCoderAuth(); return ( - + <> + {/* + * By default this text will be inert, and not be exposed anywhere + * (Sighted and blind users won't be able to interact with it). To enable + * it for screen readers, a consuming component will need bind an ID to + * another component via aria-describedby and then pass the same ID down + * as props. + */} + + {/* Slightly awkward syntax with the IIFE, but need something switch-like to make sure that all status cases are handled exhaustively */} {(() => { @@ -69,9 +47,7 @@ export const CoderAuthWrapper = ({ children, type }: WrapperProps) => { case 'authenticated': case 'distrustedWithGracePeriod': { - throw new Error( - 'Tried to process authenticated user after main content should already be shown', - ); + return ; } default: { @@ -79,7 +55,7 @@ export const CoderAuthWrapper = ({ children, type }: WrapperProps) => { } } })()} - + ); }; diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthInputForm.tsx similarity index 98% rename from plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx rename to plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthInputForm.tsx index f7e926b2..ae527e28 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthInputForm.tsx @@ -3,7 +3,7 @@ import { useId } from '../../hooks/hookPolyfills'; import { type CoderAuthStatus, useCoderAppConfig, - useCoderAuth, + useInternalCoderAuth, } from '../CoderProvider'; import { CoderLogo } from '../CoderLogo'; @@ -49,7 +49,7 @@ export const CoderAuthInputForm = () => { const hookId = useId(); const styles = useStyles(); const appConfig = useCoderAppConfig(); - const { status, registerNewToken } = useCoderAuth(); + const { status, registerNewToken } = useInternalCoderAuth(); const onSubmit = (event: FormEvent) => { event.preventDefault(); diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthLoadingState.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthLoadingState.tsx similarity index 100% rename from plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthLoadingState.tsx rename to plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthLoadingState.tsx diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthSuccessStatus.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthSuccessStatus.tsx new file mode 100644 index 00000000..d2c71513 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthSuccessStatus.tsx @@ -0,0 +1,61 @@ +/** + * @file In practice, this is a component that ideally shouldn't ever be seen by + * the end user. Any component rendering out CoderAuthForm should ideally be set + * up so that when a user is authenticated, the entire component will be + * unmounted before CoderAuthForm has a chance to handle successful states. + * + * But just for the sake of completion (and to remove the risk of runtime render + * errors), this component has been added to provide a form of double + * book-keeping for the auth status switch checks in the parent component. Don't + * want the entire plugin to blow up if an auth conditional in a different + * component is accidentally set up wrong. + */ +import React from 'react'; +import { makeStyles } from '@material-ui/core'; +import { CoderLogo } from '../CoderLogo'; +import { UnlinkAccountButton } from './UnlinkAccountButton'; + +const useStyles = makeStyles(theme => ({ + root: { + display: 'flex', + flexFlow: 'column nowrap', + alignItems: 'center', + rowGap: theme.spacing(1), + + maxWidth: '30em', + marginLeft: 'auto', + marginRight: 'auto', + color: theme.palette.text.primary, + fontSize: '1rem', + }, + + statusArea: { + display: 'flex', + flexFlow: 'column nowrap', + alignItems: 'center', + }, + + logo: { + // + }, + + text: { + textAlign: 'center', + lineHeight: '1rem', + }, +})); + +export function CoderAuthSuccessStatus() { + const styles = useStyles(); + + return ( +
+
+ +

You are fully authenticated with Coder!

+
+ + +
+ ); +} diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx new file mode 100644 index 00000000..63b9fdd0 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx @@ -0,0 +1,42 @@ +import React, { type ComponentProps } from 'react'; +import { LinkButton } from '@backstage/core-components'; +import { makeStyles } from '@material-ui/core'; +import { useInternalCoderAuth } from '../CoderProvider'; + +type Props = Readonly, 'to'>>; + +const useStyles = makeStyles(() => ({ + root: { + display: 'block', + maxWidth: 'fit-content', + }, +})); + +export function UnlinkAccountButton({ + className, + onClick, + type = 'button', + ...delegatedProps +}: Props) { + const styles = useStyles(); + const { ejectToken } = useInternalCoderAuth(); + + return ( + { + ejectToken(); + onClick?.(event); + }} + {...delegatedProps} + > + Unlink Coder account + + ); +} diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/index.ts b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/index.ts new file mode 100644 index 00000000..752873c4 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/index.ts @@ -0,0 +1 @@ +export * from './CoderAuthForm'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/CoderAuthFormCardWrapper.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/CoderAuthFormCardWrapper.test.tsx new file mode 100644 index 00000000..2a0c7cb1 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/CoderAuthFormCardWrapper.test.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { CoderProviderWithMockAuth } from '../../testHelpers/setup'; +import type { CoderAuthStatus } from '../CoderProvider'; +import { + mockAppConfig, + mockAuthStates, +} from '../../testHelpers/mockBackstageData'; +import { CoderAuthFormCardWrapper } from './CoderAuthFormCardWrapper'; +import { renderInTestApp } from '@backstage/test-utils'; + +type RenderInputs = Readonly<{ + authStatus: CoderAuthStatus; + childButtonText: string; +}>; + +async function renderAuthWrapper({ + authStatus, + childButtonText, +}: RenderInputs) { + return renderInTestApp( + + + + + , + ); +} + +describe(`${CoderAuthFormCardWrapper.name}`, () => { + it('Displays the main children when the user is authenticated', async () => { + const childButtonText = 'I have secret Coder content!'; + const validStatuses: readonly CoderAuthStatus[] = [ + 'authenticated', + 'distrustedWithGracePeriod', + ]; + + for (const authStatus of validStatuses) { + const { unmount } = await renderAuthWrapper({ + authStatus, + childButtonText, + }); + + const button = await screen.findByRole('button', { + name: childButtonText, + }); + + // This assertion isn't necessary because findByRole will throw an error + // if the button can't be found within the expected period of time. Doing + // this purely to make the Backstage linter happy + expect(button).toBeInTheDocument(); + unmount(); + } + }); + + it('Hides the main children for any invalid/untrustworthy auth status', async () => { + const childButtonText = 'I should never be visible on the screen!'; + const invalidStatuses: readonly CoderAuthStatus[] = [ + 'deploymentUnavailable', + 'distrusted', + 'initializing', + 'invalid', + 'noInternetConnection', + 'tokenMissing', + ]; + + for (const authStatus of invalidStatuses) { + const { unmount } = await renderAuthWrapper({ + authStatus, + childButtonText, + }); + + const button = screen.queryByRole('button', { name: childButtonText }); + expect(button).not.toBeInTheDocument(); + unmount(); + } + }); + + it('Will go back to hiding content if auth state becomes invalid after re-renders', async () => { + const buttonText = "Now you see me, now you don't"; + const { rerender } = await renderAuthWrapper({ + authStatus: 'authenticated', + childButtonText: buttonText, + }); + + // Capture button after it first appears on the screen; findBy will throw if + // the button is not actually visible + const button = await screen.findByRole('button', { name: buttonText }); + + rerender( + + + + + , + ); + + // Assert that the button is gone after the re-render flushes + expect(button).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/CoderAuthFormCardWrapper.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/CoderAuthFormCardWrapper.tsx new file mode 100644 index 00000000..1fa0f9fc --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/CoderAuthFormCardWrapper.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { A11yInfoCard, A11yInfoCardProps } from '../A11yInfoCard'; +import { useInternalCoderAuth } from '../CoderProvider'; +import { + type CoderAuthFormProps, + CoderAuthForm, +} from '../CoderAuthForm/CoderAuthForm'; +import { makeStyles } from '@material-ui/core'; + +type Props = A11yInfoCardProps & CoderAuthFormProps; + +const useStyles = makeStyles(theme => ({ + root: { + paddingTop: theme.spacing(6), + paddingBottom: theme.spacing(6), + }, +})); + +export function CoderAuthFormCardWrapper({ + children, + headerContent, + descriptionId, + ...delegatedCardProps +}: Props) { + const { isAuthenticated } = useInternalCoderAuth(); + const styles = useStyles(); + + return ( + Authenticate with Coder + } + {...delegatedCardProps} + > + {isAuthenticated ? ( + <>{children} + ) : ( +
+ +
+ )} +
+ ); +} diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/index.ts b/plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/index.ts new file mode 100644 index 00000000..e59d2626 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthFormCardWrapper/index.ts @@ -0,0 +1 @@ +export * from './CoderAuthFormCardWrapper'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthFormDialog/CoderAuthFormDialog.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthFormDialog/CoderAuthFormDialog.tsx new file mode 100644 index 00000000..7c39fc95 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthFormDialog/CoderAuthFormDialog.tsx @@ -0,0 +1,145 @@ +import React, { type HTMLAttributes, useState } from 'react'; +import { useId } from '../../hooks/hookPolyfills'; +import { makeStyles } from '@material-ui/core'; +import { LinkButton } from '@backstage/core-components'; +import Dialog from '@material-ui/core/Dialog'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogActions from '@material-ui/core/DialogActions'; +import { CoderAuthForm } from '../CoderAuthForm/CoderAuthForm'; + +const useStyles = makeStyles(theme => ({ + trigger: { + cursor: 'pointer', + color: theme.palette.primary.contrastText, + backgroundColor: theme.palette.primary.main, + width: 'fit-content', + border: 'none', + fontWeight: 600, + borderRadius: theme.shape.borderRadius, + transition: '10s color ease-in-out', + padding: `${theme.spacing(1.5)}px ${theme.spacing(2)}px`, + boxShadow: theme.shadows[10], + + '&:hover': { + backgroundColor: theme.palette.primary.dark, + boxShadow: theme.shadows[15], + }, + }, + + dialogContainer: { + width: '100%', + height: '100%', + display: 'flex', + flexFlow: 'column nowrap', + justifyContent: 'center', + alignItems: 'center', + }, + + dialogPaper: { + width: '100%', + }, + + dialogTitle: { + fontSize: '24px', + borderBottom: `${theme.palette.divider} 1px solid`, + padding: `${theme.spacing(1)}px ${theme.spacing(3)}px`, + }, + + contentContainer: { + padding: `${theme.spacing(6)}px ${theme.spacing(3)}px 0`, + }, + + actionsRow: { + display: 'flex', + flexFlow: 'row nowrap', + justifyContent: 'center', + padding: `${theme.spacing(1)}px ${theme.spacing(2)}px ${theme.spacing( + 6, + )}px`, + }, + + closeButton: { + letterSpacing: '0.05em', + padding: `${theme.spacing(0.5)}px ${theme.spacing(1)}px`, + color: theme.palette.primary.main, + + '&:hover': { + textDecoration: 'none', + }, + }, +})); + +type DialogProps = Readonly< + Omit, 'onClick' | 'className'> & { + open?: boolean; + onOpen?: () => void; + onClose?: () => void; + triggerClassName?: string; + } +>; + +export function CoderAuthFormDialog({ + children, + onOpen, + onClose, + triggerClassName, + open: outerIsOpen, +}: DialogProps) { + const hookId = useId(); + const styles = useStyles(); + const [innerIsOpen, setInnerIsOpen] = useState(false); + + const handleClose = () => { + setInnerIsOpen(false); + onClose?.(); + }; + + const isOpen = outerIsOpen ?? innerIsOpen; + const titleId = `${hookId}-dialog-title`; + const descriptionId = `${hookId}-dialog-description`; + + return ( + <> + + + + + Authenticate with Coder + + + + + + + + + Close + + + + + ); +} diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthFormDialog/index.ts b/plugins/backstage-plugin-coder/src/components/CoderAuthFormDialog/index.ts new file mode 100644 index 00000000..3b1069e3 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthFormDialog/index.ts @@ -0,0 +1 @@ +export * from './CoderAuthFormDialog'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/index.ts b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/index.ts deleted file mode 100644 index 3d0896b5..00000000 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './CoderAuthWrapper'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index 852abce1..c9b6fbb1 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -1,24 +1,34 @@ import React, { type PropsWithChildren, createContext, + useCallback, useContext, useEffect, + useLayoutEffect, + useRef, useState, } from 'react'; - +import { createPortal } from 'react-dom'; import { + type QueryCacheNotifyEvent, type UseQueryResult, useQuery, useQueryClient, } from '@tanstack/react-query'; +import { useApi } from '@backstage/core-plugin-api'; +import { type Theme, makeStyles } from '@material-ui/core'; +import { useId } from '../../hooks/hookPolyfills'; import { BackstageHttpError } from '../../api/errors'; import { CODER_QUERY_KEY_PREFIX, sharedAuthQueryKey, } from '../../api/queryOptions'; import { coderClientApiRef } from '../../api/CoderClient'; -import { useApi } from '@backstage/core-plugin-api'; +import { CoderLogo } from '../CoderLogo'; +import { CoderAuthFormDialog } from '../CoderAuthFormDialog'; +const BACKSTAGE_APP_ROOT_ID = '#root'; +const FALLBACK_UI_OVERRIDE_CLASS_NAME = 'backstage-root-override'; const TOKEN_STORAGE_KEY = 'coder-backstage-plugin/token'; // Handles auth edge case where a previously-valid token can't be verified. Not @@ -55,52 +65,28 @@ export type CoderAuthStatus = AuthState['status']; export type CoderAuth = Readonly< AuthState & { isAuthenticated: boolean; - tokenLoadedOnMount: boolean; registerNewToken: (newToken: string) => void; ejectToken: () => void; } >; -function isAuthValid(state: AuthState): boolean { - return ( - state.status === 'authenticated' || - state.status === 'distrustedWithGracePeriod' - ); -} - -type ValidCoderAuth = Extract< - CoderAuth, - { status: 'authenticated' | 'distrustedWithGracePeriod' } ->; - -export function assertValidCoderAuth( - auth: CoderAuth, -): asserts auth is ValidCoderAuth { - if (!isAuthValid(auth)) { - throw new Error('Coder auth is not valid'); - } -} - -export const AuthContext = createContext(null); +type TrackComponent = (componentInstanceId: string) => () => void; +export const AuthTrackingContext = createContext(null); +export const AuthStateContext = createContext(null); -export function useCoderAuth(): CoderAuth { - const contextValue = useContext(AuthContext); - if (contextValue === null) { - throw new Error( - `Hook ${useCoderAuth.name} is being called outside of CoderProvider`, - ); - } - - return contextValue; -} +const validAuthStatuses: readonly CoderAuthStatus[] = [ + 'authenticated', + 'distrustedWithGracePeriod', +]; -type CoderAuthProviderProps = Readonly>; +function useAuthState(): CoderAuth { + const [authToken, setAuthToken] = useState( + () => window.localStorage.getItem(TOKEN_STORAGE_KEY) ?? '', + ); -export const CoderAuthProvider = ({ children }: CoderAuthProviderProps) => { - // Need to split hairs, because the query object can be disabled. Only want to - // expose the initializing state if the app mounts with a token already in - // localStorage - const [authToken, setAuthToken] = useState(readAuthToken); + // Need to differentiate the current token from the token loaded on mount + // because the query object can be disabled. Only want to expose the + // initializing state if the app mounts with a token already in localStorage const [readonlyInitialAuthToken] = useState(authToken); const [isInsideGracePeriod, setIsInsideGracePeriod] = useState(true); @@ -112,6 +98,8 @@ export const CoderAuthProvider = ({ children }: CoderAuthProviderProps) => { queryFn: () => coderClient.syncToken(authToken), enabled: queryIsEnabled, keepPreviousData: queryIsEnabled, + + // Can't use !query.state.data because we want to refetch on undefined cases refetchOnWindowFocus: query => query.state.data !== false, }); @@ -123,8 +111,8 @@ export const CoderAuthProvider = ({ children }: CoderAuthProviderProps) => { }); // Mid-render state sync to avoid unnecessary re-renders that useEffect would - // introduce, especially since we don't know how costly re-renders could be in - // someone's arbitrarily-large Backstage deployment + // introduce. We don't know how costly re-renders could be in someone's + // arbitrarily-large Backstage deployment, so erring on the side of caution if (!isInsideGracePeriod && authState.status === 'authenticated') { setIsInsideGracePeriod(true); } @@ -152,13 +140,14 @@ export const CoderAuthProvider = ({ children }: CoderAuthProviderProps) => { // outside React because we let the user connect their own queryClient const queryClient = useQueryClient(); useEffect(() => { - let isRefetchingTokenQuery = false; - const queryCache = queryClient.getQueryCache(); + // Pseudo-mutex; makes sure that if we get a bunch of errors, only one + // revalidation will be processed at a time + let isRevalidatingToken = false; - const unsubscribe = queryCache.subscribe(async event => { + const revalidateTokenOnError = async (event: QueryCacheNotifyEvent) => { const queryError = event.query.state.error; const shouldRevalidate = - !isRefetchingTokenQuery && + !isRevalidatingToken && BackstageHttpError.isInstance(queryError) && queryError.status === 401; @@ -166,36 +155,125 @@ export const CoderAuthProvider = ({ children }: CoderAuthProviderProps) => { return; } - isRefetchingTokenQuery = true; + isRevalidatingToken = true; await queryClient.refetchQueries({ queryKey: sharedAuthQueryKey }); - isRefetchingTokenQuery = false; - }); + isRevalidatingToken = false; + }; + const queryCache = queryClient.getQueryCache(); + const unsubscribe = queryCache.subscribe(revalidateTokenOnError); return unsubscribe; }, [queryClient]); - return ( - { - if (newToken !== '') { - setAuthToken(newToken); - } - }, - ejectToken: () => { - window.localStorage.removeItem(TOKEN_STORAGE_KEY); - queryClient.removeQueries({ queryKey: [CODER_QUERY_KEY_PREFIX] }); - setAuthToken(''); - }, - }} - > - {children} - - ); -}; + return { + ...authState, + isAuthenticated: validAuthStatuses.includes(authState.status), + registerNewToken: newToken => { + if (newToken !== '') { + setAuthToken(newToken); + } + }, + ejectToken: () => { + setAuthToken(''); + window.localStorage.removeItem(TOKEN_STORAGE_KEY); + queryClient.removeQueries({ queryKey: [CODER_QUERY_KEY_PREFIX] }); + }, + }; +} + +type AuthFallbackState = Readonly<{ + trackComponent: TrackComponent; + hasNoAuthInputs: boolean; +}>; + +function useAuthFallbackState(): AuthFallbackState { + // Can't do state syncs or anything else that would normally minimize + // re-renders here because we have to wait for the entire application to + // complete its initial render before we can decide if we need a fallback UI + const [isMounted, setIsMounted] = useState(false); + useEffect(() => { + setIsMounted(true); + }, []); + + // Not the biggest fan of needing to keep the two pieces of state in sync, but + // setting the render state to a simple boolean rather than the whole Set + // means that we re-render only when we go from 0 trackers to 1+, or from 1+ + // trackers to 0. We don't care about the exact number of components being + // tracked - just whether we have any at all + const [hasTrackers, setHasTrackers] = useState(false); + const trackedComponentsRef = useRef>(null!); + if (trackedComponentsRef.current === null) { + trackedComponentsRef.current = new Set(); + } + + const trackComponent = useCallback((componentId: string) => { + // React will bail out of re-renders if you dispatch the same state value + // that it already has, and that's easier to guarantee since the UI state + // only has a primitive. Calling this function too often should cause no + // problems, and most calls should be a no-op + const syncTrackerToUi = () => { + setHasTrackers(trackedComponentsRef.current.size > 0); + }; + + trackedComponentsRef.current.add(componentId); + syncTrackerToUi(); + + return () => { + trackedComponentsRef.current.delete(componentId); + syncTrackerToUi(); + }; + }, []); + + return { + trackComponent, + hasNoAuthInputs: isMounted && !hasTrackers, + }; +} + +/** + * Exposes auth state for other components, but has additional logic for spying + * on consumers of the hook. + * + * Caveats: + * 1. This hook should *NEVER* be exposed to the end user + * 2. All official Coder plugin components should favor this hook over + * useEndUserCoderAuth when possible + * + * A fallback UI for letting the user input auth information will appear if + * there are no official Coder components that are able to give the user a way + * to do that through normal user flows. + */ +export function useInternalCoderAuth(): CoderAuth { + const trackComponent = useContext(AuthTrackingContext); + if (trackComponent === null) { + throw new Error('Unable to retrieve state for displaying fallback auth UI'); + } + + // Assuming trackComponent is set up properly, the values of it and instanceId + // should both be stable until whatever component is using this hook unmounts. + // Values only added to dependency array to satisfy ESLint + const instanceId = useId(); + useEffect(() => { + const cleanupTracking = trackComponent(instanceId); + return cleanupTracking; + }, [instanceId, trackComponent]); + + return useEndUserCoderAuth(); +} + +/** + * Exposes Coder auth state to the rest of the UI. + */ +// This hook should only be used by end users trying to use the Coder SDK inside +// Backstage. The hook is renamed on final export to avoid confusion +export function useEndUserCoderAuth(): CoderAuth { + const authContextValue = useContext(AuthStateContext); + if (authContextValue === null) { + throw new Error('Cannot retrieve auth information from CoderProvider'); + } + + return authContextValue; +} type GenerateAuthStateInputs = Readonly<{ authToken: string; @@ -331,6 +409,218 @@ function generateAuthState({ }; } -function readAuthToken(): string { - return window.localStorage.getItem(TOKEN_STORAGE_KEY) ?? ''; +// Have to get the root of the React application to adjust its dimensions when +// we display the fallback UI. Sadly, we can't assert that the root is always +// defined from outside a UI component, because throwing any errors here would +// blow up the entire Backstage application, and wreck all the other plugins +const mainAppRoot = document.querySelector(BACKSTAGE_APP_ROOT_ID); + +type StyleKey = 'landmarkWrapper' | 'dialogButton' | 'logo'; +type StyleProps = Readonly<{ isDialogOpen: boolean }>; + +const useFallbackStyles = makeStyles(theme => ({ + landmarkWrapper: ({ isDialogOpen }) => ({ + zIndex: isDialogOpen ? 0 : 9999, + position: 'fixed', + bottom: theme.spacing(2), + width: '100%', + maxWidth: 'fit-content', + left: '50%', + transform: 'translateX(-50%)', + }), + + dialogButton: { + display: 'flex', + flexFlow: 'row nowrap', + columnGap: theme.spacing(1), + alignItems: 'center', + }, + + logo: { + fill: theme.palette.primary.contrastText, + width: theme.spacing(3), + }, +})); + +function FallbackAuthUi() { + /** + * Add additional padding to the bottom of the main app to make sure that even + * with the fallback UI in place, it won't permanently cover up any of the + * other content as long as the user scrolls down far enough. + * + * Involves jumping through a bunch of hoops since we don't have 100% control + * over the Backstage application. Need to minimize risks of breaking existing + * Backstage styling or other plugins + */ + const fallbackRef = useRef(null); + useLayoutEffect(() => { + const fallback = fallbackRef.current; + const mainAppContainer = + mainAppRoot?.querySelector('main') ?? null; + + if (fallback === null || mainAppContainer === null) { + return undefined; + } + + // Adding a new style node lets us override the existing styles via the CSS + // cascade rather than directly modifying them, which minimizes the risks of + // breaking anything. If we were to modify the styles and try resetting them + // with the cleanup function, there's a risk the cleanup function would have + // closure over stale values and try "resetting" things to a value that is + // no longer used + const overrideStyleNode = document.createElement('style'); + overrideStyleNode.type = 'text/css'; + + // Using ComputedStyle objects because they maintain live links to computed + // properties. Plus, since most styling goes through MUI's makeStyles (which + // is based on CSS classes), trying to access properties directly off the + // nodes won't always work + const liveAppStyles = getComputedStyle(mainAppContainer); + const liveFallbackStyles = getComputedStyle(fallback); + + let prevPaddingBottom: string | undefined = undefined; + const updatePaddingForFallbackUi: MutationCallback = () => { + const prevInnerHtml = overrideStyleNode.innerHTML; + overrideStyleNode.innerHTML = ''; + const paddingBottomWithNoOverride = liveAppStyles.paddingBottom || '0px'; + + if (paddingBottomWithNoOverride === prevPaddingBottom) { + overrideStyleNode.innerHTML = prevInnerHtml; + return; + } + + // parseInt will automatically remove units from bottom property + const fallbackBottom = parseInt(liveFallbackStyles.bottom || '0', 10); + const normalized = Number.isNaN(fallbackBottom) ? 0 : fallbackBottom; + const paddingToAdd = fallback.offsetHeight + normalized; + + overrideStyleNode.innerHTML = ` + .${FALLBACK_UI_OVERRIDE_CLASS_NAME} { + padding-bottom: calc(${paddingBottomWithNoOverride} + ${paddingToAdd}px) !important; + } + `; + + // Only update prev padding after state changes have definitely succeeded + prevPaddingBottom = paddingBottomWithNoOverride; + }; + + const observer = new MutationObserver(updatePaddingForFallbackUi); + observer.observe(document.head, { childList: true }); + observer.observe(mainAppContainer, { + childList: false, + subtree: false, + attributes: true, + attributeFilter: ['class', 'style'], + }); + + // Applying mutations after we've started observing will trigger the + // callback, but as long as it's set up properly, the user shouldn't notice. + // Also serves a way to ensure the mutation callback runs at least once + document.head.append(overrideStyleNode); + mainAppContainer.classList.add(FALLBACK_UI_OVERRIDE_CLASS_NAME); + + return () => { + // Be sure to disconnect observer before applying other cleanup mutations + observer.disconnect(); + overrideStyleNode.remove(); + mainAppContainer.classList.remove(FALLBACK_UI_OVERRIDE_CLASS_NAME); + }; + }, []); + + const hookId = useId(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const styles = useFallbackStyles({ isDialogOpen }); + + // Wrapping fallback button in landmark so that screen reader users can jump + // straight to the button from a screen reader directory rotor, and don't have + // to navigate through every single other element first + const landmarkId = `${hookId}-landmark`; + const fallbackUi = ( +
+ + + setIsDialogOpen(true)} + onClose={() => setIsDialogOpen(false)} + triggerClassName={styles.dialogButton} + > + + Authenticate with Coder + +
+ ); + + return createPortal(fallbackUi, document.body); +} + +/** + * Sorry about how wacky this approach is, but this setup should simplify the + * code literally everywhere else in the plugin. + * + * The setup is that we have two versions of the tracking context: one that has + * the live trackComponent function, and one that has the dummy. The main parts + * of the UI get the live version, and the parts of the UI that deal with the + * fallback auth UI get the dummy version. + * + * By having two contexts, we can dynamically expose or hide the tracking + * state for any components that use any version of the Coder auth state. All + * other components can use the same hook without being aware of where they're + * being mounted. That means you can use the exact same components in either + * region without needing to rewrite anything outside this file. + * + * Any other component that uses useInternalCoderAuth will reach up the + * component tree until it can grab *some* kind of tracking function. The hook + * only cares about whether it got a function at all; it doesn't care about what + * it does. The hook will call the function either way, but only the components + * in the "live" region will influence whether the fallback UI should be + * displayed. + * + * Dummy function defined outside the component to prevent risk of needless + * re-renders through Context. + */ + +/** + * A dummy version of the component tracker function. + * + * In production, this is used to define a dummy version of the context + * dependency for the "fallback auth UI" portion of the app. + * + * In testing, this is used for the vast majority of component tests to provide + * the tracker dependency and make sure that the components can properly render + * without having to be wired up to the entire plugin. + */ +export const dummyTrackComponent: TrackComponent = () => { + // Deliberately perform a no-op on initial call + return () => { + // And deliberately perform a no-op on cleanup + }; +}; + +export function CoderAuthProvider({ + children, +}: Readonly>) { + const authState = useAuthState(); + const { hasNoAuthInputs, trackComponent } = useAuthFallbackState(); + const needFallbackUi = !authState.isAuthenticated && hasNoAuthInputs; + + return ( + + + {children} + + + {needFallbackUi && ( + + + + )} + + ); } diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx index 955aae28..73acc13c 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx @@ -12,7 +12,7 @@ import { import { CoderProvider } from './CoderProvider'; import { useCoderAppConfig } from './CoderAppConfigProvider'; -import { type CoderAuth, useCoderAuth } from './CoderAuthProvider'; +import { type CoderAuth, useInternalCoderAuth } from './CoderAuthProvider'; import { getMockConfigApi, @@ -56,7 +56,7 @@ describe(`${CoderProvider.name}`, () => { describe('Auth', () => { // Can't use the render helpers because they all assume that the auth isn't // core to the functionality. In this case, you do need to bring in the full - // CoderProvider + // CoderProvider to make sure that it's working properly const renderUseCoderAuth = () => { const discoveryApi = getMockDiscoveryApi(); const configApi = getMockConfigApi(); @@ -70,7 +70,7 @@ describe(`${CoderProvider.name}`, () => { apis: { urlSync, identityApi }, }); - return renderHook(useCoderAuth, { + return renderHook(useInternalCoderAuth, { wrapper: ({ children }) => ( { const styles = useStyles(); return ( - - - - - - } - /> - + + + + + } + /> + } + {...props} + >
diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx index 57a41922..3d9dbcf6 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx @@ -7,7 +7,7 @@ import React, { } from 'react'; import { useId } from '../../hooks/hookPolyfills'; -import { useCoderAuth } from '../CoderProvider'; +import { useInternalCoderAuth } from '../CoderProvider'; import { useWorkspacesCardContext } from './Root'; import { VisuallyHidden } from '../VisuallyHidden'; @@ -102,7 +102,7 @@ export const ExtraActionsButton = ({ const hookId = useId(); const [loadedAnchor, setLoadedAnchor] = useState(); const refreshWorkspaces = useRefreshWorkspaces(); - const { ejectToken } = useCoderAuth(); + const { ejectToken } = useInternalCoderAuth(); const styles = useStyles(); const closeMenu = () => setLoadedAnchor(undefined); diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/HeaderRow.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/HeaderRow.tsx index 8c67d5e5..b96f2361 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/HeaderRow.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/HeaderRow.tsx @@ -1,50 +1,37 @@ import React, { HTMLAttributes, ReactNode } from 'react'; -import { Theme, makeStyles } from '@material-ui/core'; +import { type Theme, makeStyles } from '@material-ui/core'; import { useWorkspacesCardContext } from './Root'; +import type { HtmlHeader } from '../../typesConstants'; type StyleKey = 'root' | 'header' | 'hgroup' | 'subheader'; - -type MakeStylesInputs = Readonly<{ - fullBleedLayout: boolean; -}>; - -const useStyles = makeStyles(theme => ({ - root: ({ fullBleedLayout }) => ({ +const useStyles = makeStyles(theme => ({ + root: { color: theme.palette.text.primary, display: 'flex', flexFlow: 'row nowrap', alignItems: 'center', gap: theme.spacing(1), - - // Have to jump through some hoops for the border; have to extend out the - // root to make sure that the border stretches all the way across the - // parent, and then add padding back to just the main content - borderBottom: `1px solid ${theme.palette.divider}`, - marginLeft: fullBleedLayout ? `-${theme.spacing(2)}px` : 0, - marginRight: fullBleedLayout ? `-${theme.spacing(2)}px` : 0, - padding: `0 ${theme.spacing(2)}px ${theme.spacing(2)}px ${theme.spacing( - 2.5, - )}px`, - }), + }, hgroup: { marginRight: 'auto', }, header: { - fontSize: '24px', + fontSize: '1.5rem', lineHeight: 1, margin: 0, }, subheader: { margin: '0', + fontSize: '0.875rem', + fontWeight: 400, color: theme.palette.text.secondary, paddingTop: theme.spacing(0.5), }, })); -type HtmlHeader = `h${1 | 2 | 3 | 4 | 5 | 6}`; type ClassName = `${Exclude}ClassName`; type HeaderProps = Readonly< @@ -67,11 +54,10 @@ export const HeaderRow = ({ subheaderClassName, activeRepoFilteringText, headerText = 'Coder Workspaces', - fullBleedLayout = true, ...delegatedProps }: HeaderProps) => { const { headerId, workspacesConfig } = useWorkspacesCardContext(); - const styles = useStyles({ fullBleedLayout }); + const styles = useStyles(); const HeadingComponent = headerLevel ?? 'h2'; const { repoUrl } = workspacesConfig; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx index 9a2d118f..0866d95a 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx @@ -4,6 +4,7 @@ */ import React, { type HTMLAttributes, + type ReactNode, createContext, useContext, useState, @@ -14,11 +15,9 @@ import { useCoderWorkspacesConfig, type CoderWorkspacesConfig, } from '../../hooks/useCoderWorkspacesConfig'; - import type { Workspace } from '../../typesConstants'; import { useCoderWorkspacesQuery } from '../../hooks/useCoderWorkspacesQuery'; -import { Card } from '../Card'; -import { CoderAuthWrapper } from '../CoderAuthWrapper'; +import { CoderAuthFormCardWrapper } from '../CoderAuthFormCardWrapper'; export type WorkspacesQuery = UseQueryResult; @@ -40,12 +39,14 @@ export type WorkspacesCardProps = Readonly< defaultQueryFilter?: string; onFilterChange?: (newFilter: string) => void; readEntityData?: boolean; + headerContent?: ReactNode; } >; const InnerRoot = ({ children, className, + headerContent, queryFilter: outerFilter, onFilterChange: onOuterFilterChange, defaultQueryFilter = 'owner:me', @@ -65,44 +66,49 @@ const InnerRoot = ({ const headerId = `${hookId}-header`; return ( - - { - setInnerFilter(newFilter); - onOuterFilterChange?.(newFilter); - }, - }} + { + setInnerFilter(newFilter); + onOuterFilterChange?.(newFilter); + }, + }} + > + - {/* - * 2024-01-31: This output is a
, but that should be changed to a - * once that element is supported by more browsers. Setting up - * accessibility markup and landmark behavior manually in the meantime - */} - - {/* Want to expose the overall container as a form for good - semantics and screen reader support, but since there isn't an - explicit submission process (queries happen automatically), it - felt better to use a
with a role override to side-step edge - cases around keyboard input and button children that native
- elements automatically introduce */} -
{children}
- - - + {/* Want to expose the overall container as a form for good + semantics and screen reader support, but since there isn't an + explicit submission process (queries happen automatically), it + felt better to use a
with a role override to side-step edge + cases around keyboard input and button children that native + elements automatically introduce */} +
{children}
+ + ); }; export function Root(props: WorkspacesCardProps) { - // Doing this to insulate the user from needing to worry about accidentally - // flipping the value of readEntityData between renders. If this value - // changes, it will cause the component to unmount and remount, but that - // should be painless/maybe invisible compared to having the component throw - // a full error and triggering an error boundary + /** + * Binding the value of readEntityData as a render key to make using the + * component less painful to use overall for end users. + * + * Without this, the component will throw an error anytime the user flips the + * value of readEntityData from false to true, or vice-versa. + * + * With a render key, whenever the key changes, the whole component will + * unmount and then remount. This isn't a problem because all its important + * state is stored outside React via React Query, so on the remount, it can + * reuse the existing state and just has rebuild itself via the new props. + */ const renderKey = String(props.readEntityData ?? false); return ; } diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts index a3b22d3d..4e41ef86 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { workspaces, workspacesByRepo } from '../api/queryOptions'; import type { CoderWorkspacesConfig } from './useCoderWorkspacesConfig'; import { useCoderSdk } from './useCoderSdk'; -import { useCoderAuth } from '../components/CoderProvider'; +import { useInternalCoderAuth } from '../components/CoderProvider'; type QueryInput = Readonly<{ coderQuery: string; @@ -13,7 +13,7 @@ export function useCoderWorkspacesQuery({ coderQuery, workspacesConfig, }: QueryInput) { - const auth = useCoderAuth(); + const auth = useInternalCoderAuth(); const coderSdk = useCoderSdk(); const hasRepoData = workspacesConfig && workspacesConfig.repoUrl; diff --git a/plugins/backstage-plugin-coder/src/plugin.ts b/plugins/backstage-plugin-coder/src/plugin.ts index 5dad65dc..2aaaab89 100644 --- a/plugins/backstage-plugin-coder/src/plugin.ts +++ b/plugins/backstage-plugin-coder/src/plugin.ts @@ -58,16 +58,6 @@ export const CoderProvider = coderPlugin.provide( }), ); -export const CoderAuthWrapper = coderPlugin.provide( - createComponentExtension({ - name: 'CoderAuthWrapper', - component: { - lazy: () => - import('./components/CoderAuthWrapper').then(m => m.CoderAuthWrapper), - }, - }), -); - export const CoderErrorBoundary = coderPlugin.provide( createComponentExtension({ name: 'CoderErrorBoundary', @@ -192,12 +182,17 @@ export const CoderWorkspacesReminderAccordion = coderPlugin.provide( ); /** - * All custom hooks exposed by the plugin. + * Custom hooks needed for some of the custom Coder components */ -export { useCoderWorkspacesConfig } from './hooks/useCoderWorkspacesConfig'; -export { useCoderWorkspacesQuery } from './hooks/useCoderWorkspacesQuery'; export { useWorkspacesCardContext } from './components/CoderWorkspacesCard/Root'; +/** + * General custom hooks that can be used in various places. + */ +export { useCoderWorkspacesConfig } from './hooks/useCoderWorkspacesConfig'; +export { useCoderSdk } from './hooks/useCoderSdk'; +export { useEndUserCoderAuth as useCoderAuth } from './components/CoderProvider/CoderAuthProvider'; + /** * All custom types */ diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts index 28e258f5..34f11218 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts @@ -159,7 +159,6 @@ export const mockCoderWorkspacesConfig = (() => { const authedState = { token: mockCoderAuthToken, error: undefined, - tokenLoadedOnMount: true, isAuthenticated: true, registerNewToken: jest.fn(), ejectToken: jest.fn(), @@ -168,7 +167,6 @@ const authedState = { const notAuthedState = { token: undefined, error: undefined, - tokenLoadedOnMount: false, isAuthenticated: false, registerNewToken: jest.fn(), ejectToken: jest.fn(), diff --git a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx index 0cef032f..86ceedcb 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx +++ b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx @@ -17,8 +17,10 @@ import { type CoderAuthStatus, type CoderAppConfig, type CoderProviderProps, - AuthContext, + AuthStateContext, + AuthTrackingContext, CoderAppConfigProvider, + dummyTrackComponent, } from '../components/CoderProvider'; import { mockAppConfig, @@ -128,9 +130,11 @@ export const CoderProviderWithMockAuth = ({ - - {children} - + + + {children} + + @@ -164,7 +168,9 @@ export const renderHookAsCoderEntity = async < queryClient={mockQueryClient} authStatus={authStatus} > - {children} + + <>{children} + ); diff --git a/plugins/backstage-plugin-coder/src/typesConstants.ts b/plugins/backstage-plugin-coder/src/typesConstants.ts index d9922920..76551f89 100644 --- a/plugins/backstage-plugin-coder/src/typesConstants.ts +++ b/plugins/backstage-plugin-coder/src/typesConstants.ts @@ -103,3 +103,11 @@ export type User = Readonly<{ username: string; avatar_url: string; }>; + +/** + * 2024-05-22 - While this isn't documented anywhere, TanStack Query defaults to + * retrying a failed API request 3 times before exposing an error to the UI + */ +export const DEFAULT_TANSTACK_QUERY_RETRY_COUNT = 3; + +export type HtmlHeader = `h${1 | 2 | 3 | 4 | 5 | 6}`; diff --git a/yarn.lock b/yarn.lock index b13b38c9..e7553d7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8719,9 +8719,9 @@ integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== "@types/react-dom@*", "@types/react-dom@^18.0.0": - version "18.2.21" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.21.tgz#b8c81715cebdebb2994378616a8d54ace54f043a" - integrity sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw== + version "18.3.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" + integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== dependencies: "@types/react" "*" @@ -8757,12 +8757,11 @@ "@types/react" "*" "@types/react@*", "@types/react@^16.13.1 || ^17.0.0 || ^18.0.0": - version "18.2.64" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.64.tgz#3700fbb6b2fa60a6868ec1323ae4cbd446a2197d" - integrity sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg== + version "18.3.2" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.2.tgz#462ae4904973bc212fa910424d901e3d137dbfcd" + integrity sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w== dependencies: "@types/prop-types" "*" - "@types/scheduler" "*" csstype "^3.0.2" "@types/react@^16.13.1 || ^17.0.0": @@ -8801,7 +8800,7 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/scheduler@*", "@types/scheduler@^0.16": +"@types/scheduler@^0.16": version "0.16.8" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== @@ -20249,6 +20248,14 @@ react-dom@^18.0.2: loose-envify "^1.1.0" scheduler "^0.23.0" +react-dom@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + react-double-scrollbar@0.0.15: version "0.0.15" resolved "https://registry.yarnpkg.com/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz#e915ab8cb3b959877075f49436debfdb04288fe4" @@ -21181,6 +21188,13 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + schema-utils@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" @@ -21918,16 +21932,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -22001,7 +22006,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -22015,13 +22020,6 @@ strip-ansi@5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -23830,7 +23828,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -23848,15 +23846,6 @@ wrap-ansi@^6.0.1: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 06d24da68166bec88962153ad2256798ba6c2a67 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 31 May 2024 11:36:29 -0400 Subject: [PATCH 2/9] chore(Coder plugin): import preview version of Coder SDK into plugin (#130) * chore: add vendored version of experimental Coder SDK * fix: improve data hiding for CoderSdk * docs: update typo * fix: add additional properties to hide from SDK --- plugins/backstage-plugin-coder/package.json | 3 + .../src/api/vendoredSdk/README.md | 20 + .../src/api/vendoredSdk/api/api.ts | 1940 ++++++++++++ .../src/api/vendoredSdk/api/errors.ts | 124 + .../src/api/vendoredSdk/api/typesGenerated.ts | 2599 +++++++++++++++++ .../src/api/vendoredSdk/index.ts | 36 + .../src/api/vendoredSdk/utils/delay.ts | 4 + yarn.lock | 30 +- 8 files changed, 4741 insertions(+), 15 deletions(-) create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/README.md create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/api/errors.ts create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/api/typesGenerated.ts create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/utils/delay.ts diff --git a/plugins/backstage-plugin-coder/package.json b/plugins/backstage-plugin-coder/package.json index e21caf74..1d21b960 100644 --- a/plugins/backstage-plugin-coder/package.json +++ b/plugins/backstage-plugin-coder/package.json @@ -42,6 +42,8 @@ "@material-ui/lab": "4.0.0-alpha.61", "@tanstack/react-query": "4.36.1", "axios": "^1.6.8", + "dayjs": "^1.11.11", + "ua-parser-js": "^1.0.37", "use-sync-external-store": "^1.2.1", "valibot": "^0.28.1" }, @@ -57,6 +59,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.0.0", + "@types/ua-parser-js": "^0.7.39", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/README.md b/plugins/backstage-plugin-coder/src/api/vendoredSdk/README.md new file mode 100644 index 00000000..354acb1c --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/README.md @@ -0,0 +1,20 @@ +# Coder SDK - Experimental Vendored Version + +This is a vendored version of the main API files from the +[core Coder OSS repo](https://github.com/coder/coder/tree/main/site/src/api). All files (aside from test files) have been copied over directly, with only a +few changes made to satisfy default Backstage ESLint rules. + +While there is a risk of this getting out of sync with the versions of the +files in Coder OSS, the Coder API itself should be treated as stable. Breaking +changes are only made when absolutely necessary. + +## General approach + +- Copy over relevant files from Coder OSS and place them in relevant folders + - As much as possible, the file structure of the vendored files should match the file structure of Coder OSS to make it easier to copy updated files over. +- Have a single file at the top level of this directory that exports out the files for consumption elsewhere in the plugin. No plugin code should interact with the vendored files directly. + +## Eventual plans + +Coder has eventual plans to create a true SDK published through NPM. Once +that is published, all of this vendored code should be removed in favor of it. diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts new file mode 100644 index 00000000..e0eafd1d --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts @@ -0,0 +1,1940 @@ +/** + * @file Coder is starting to import the Coder API file into more and more + * external projects, as a "pseudo-SDK". We are not at a stage where we are + * ready to commit to maintaining a public SDK, but we need equivalent + * functionality in other places. + * + * Message somebody from Team Blueberry if you need more context, but so far, + * these projects are importing the file: + * + * - The Coder VS Code extension + * @see {@link https://github.com/coder/vscode-coder} + * - The Coder Backstage plugin + * @see {@link https://github.com/coder/backstage-plugins} + * + * It is important that this file not do any aliased imports, or else the other + * consumers could break (particularly for platforms that limit how much you can + * touch their configuration files, like Backstage). Relative imports are still + * safe, though. + * + * For example, `utils/delay` must be imported using `../utils/delay` instead. + */ +import globalAxios, { type AxiosInstance, isAxiosError } from 'axios'; +import type dayjs from 'dayjs'; +import userAgentParser from 'ua-parser-js'; +import { delay } from '../utils/delay'; +import * as TypesGen from './typesGenerated'; + +const getMissingParameters = ( + oldBuildParameters: TypesGen.WorkspaceBuildParameter[], + newBuildParameters: TypesGen.WorkspaceBuildParameter[], + templateParameters: TypesGen.TemplateVersionParameter[], +) => { + const missingParameters: TypesGen.TemplateVersionParameter[] = []; + const requiredParameters: TypesGen.TemplateVersionParameter[] = []; + + templateParameters.forEach(p => { + // It is mutable and required. Mutable values can be changed after so we + // don't need to ask them if they are not required. + const isMutableAndRequired = p.mutable && p.required; + // Is immutable, so we can check if it is its first time on the build + const isImmutable = !p.mutable; + + if (isMutableAndRequired || isImmutable) { + requiredParameters.push(p); + } + }); + + for (const parameter of requiredParameters) { + // Check if there is a new value + let buildParameter = newBuildParameters.find( + p => p.name === parameter.name, + ); + + // If not, get the old one + if (!buildParameter) { + buildParameter = oldBuildParameters.find(p => p.name === parameter.name); + } + + // If there is a value from the new or old one, it is not missed + if (buildParameter) { + continue; + } + + missingParameters.push(parameter); + } + + // Check if parameter "options" changed and we can't use old build parameters. + templateParameters.forEach(templateParameter => { + if (templateParameter.options.length === 0) { + return; + } + + // Check if there is a new value + let buildParameter = newBuildParameters.find( + p => p.name === templateParameter.name, + ); + + // If not, get the old one + if (!buildParameter) { + buildParameter = oldBuildParameters.find( + p => p.name === templateParameter.name, + ); + } + + if (!buildParameter) { + return; + } + + const matchingOption = templateParameter.options.find( + option => option.value === buildParameter?.value, + ); + if (!matchingOption) { + missingParameters.push(templateParameter); + } + }); + return missingParameters; +}; + +/** + * + * @param agentId + * @returns An EventSource that emits agent metadata event objects + * (ServerSentEvent) + */ +export const watchAgentMetadata = (agentId: string): EventSource => { + return new EventSource( + `${location.protocol}//${location.host}/api/v2/workspaceagents/${agentId}/watch-metadata`, + { withCredentials: true }, + ); +}; + +/** + * @returns {EventSource} An EventSource that emits workspace event objects + * (ServerSentEvent) + */ +export const watchWorkspace = (workspaceId: string): EventSource => { + return new EventSource( + `${location.protocol}//${location.host}/api/v2/workspaces/${workspaceId}/watch`, + { withCredentials: true }, + ); +}; + +export const getURLWithSearchParams = ( + basePath: string, + options?: SearchParamOptions, +): string => { + if (!options) { + return basePath; + } + + const searchParams = new URLSearchParams(); + const keys = Object.keys(options) as (keyof SearchParamOptions)[]; + keys.forEach(key => { + const value = options[key]; + if (value !== undefined && value !== '') { + searchParams.append(key, value.toString()); + } + }); + + const searchString = searchParams.toString(); + return searchString ? `${basePath}?${searchString}` : basePath; +}; + +// withDefaultFeatures sets all unspecified features to not_entitled and +// disabled. +export const withDefaultFeatures = ( + fs: Partial, +): TypesGen.Entitlements['features'] => { + for (const feature of TypesGen.FeatureNames) { + // Skip fields that are already filled. + if (fs[feature] !== undefined) { + continue; + } + + fs[feature] = { + enabled: false, + entitlement: 'not_entitled', + }; + } + + return fs as TypesGen.Entitlements['features']; +}; + +type WatchBuildLogsByTemplateVersionIdOptions = { + after?: number; + onMessage: (log: TypesGen.ProvisionerJobLog) => void; + onDone?: () => void; + onError: (error: Error) => void; +}; + +export const watchBuildLogsByTemplateVersionId = ( + versionId: string, + { + onMessage, + onDone, + onError, + after, + }: WatchBuildLogsByTemplateVersionIdOptions, +) => { + const searchParams = new URLSearchParams({ follow: 'true' }); + if (after !== undefined) { + searchParams.append('after', after.toString()); + } + + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; + const socket = new WebSocket( + `${proto}//${ + location.host + }/api/v2/templateversions/${versionId}/logs?${searchParams.toString()}`, + ); + + socket.binaryType = 'blob'; + + socket.addEventListener('message', event => + onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), + ); + + socket.addEventListener('error', () => { + onError(new Error('Connection for logs failed.')); + socket.close(); + }); + + socket.addEventListener('close', () => { + // When the socket closes, logs have finished streaming! + onDone?.(); + }); + + return socket; +}; + +export const watchWorkspaceAgentLogs = ( + agentId: string, + { after, onMessage, onDone, onError }: WatchWorkspaceAgentLogsOptions, +) => { + // WebSocket compression in Safari (confirmed in 16.5) is broken when + // the server sends large messages. The following error is seen: + // + // WebSocket connection to 'wss://.../logs?follow&after=0' failed: The operation couldn’t be completed. Protocol error + // + const noCompression = + userAgentParser(navigator.userAgent).browser.name === 'Safari' + ? '&no_compression' + : ''; + + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; + const socket = new WebSocket( + `${proto}//${location.host}/api/v2/workspaceagents/${agentId}/logs?follow&after=${after}${noCompression}`, + ); + socket.binaryType = 'blob'; + + socket.addEventListener('message', event => { + const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentLog[]; + onMessage(logs); + }); + + socket.addEventListener('error', () => { + onError(new Error('socket errored')); + }); + + socket.addEventListener('close', () => { + onDone?.(); + }); + + return socket; +}; + +type WatchWorkspaceAgentLogsOptions = { + after: number; + onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void; + onDone?: () => void; + onError: (error: Error) => void; +}; + +type WatchBuildLogsByBuildIdOptions = { + after?: number; + onMessage: (log: TypesGen.ProvisionerJobLog) => void; + onDone?: () => void; + onError?: (error: Error) => void; +}; +export const watchBuildLogsByBuildId = ( + buildId: string, + { onMessage, onDone, onError, after }: WatchBuildLogsByBuildIdOptions, +) => { + const searchParams = new URLSearchParams({ follow: 'true' }); + if (after !== undefined) { + searchParams.append('after', after.toString()); + } + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; + const socket = new WebSocket( + `${proto}//${ + location.host + }/api/v2/workspacebuilds/${buildId}/logs?${searchParams.toString()}`, + ); + socket.binaryType = 'blob'; + + socket.addEventListener('message', event => + onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), + ); + + socket.addEventListener('error', () => { + onError?.(new Error('Connection for logs failed.')); + socket.close(); + }); + + socket.addEventListener('close', () => { + // When the socket closes, logs have finished streaming! + onDone?.(); + }); + + return socket; +}; + +// This is the base header that is used for several requests. This is defined as +// a readonly value, but only copies of it should be passed into the API calls, +// because Axios is able to mutate the headers +const BASE_CONTENT_TYPE_JSON = { + 'Content-Type': 'application/json', +} as const satisfies HeadersInit; + +type TemplateOptions = Readonly<{ + readonly deprecated?: boolean; +}>; + +type SearchParamOptions = TypesGen.Pagination & { + q?: string; +}; + +type RestartWorkspaceParameters = Readonly<{ + workspace: TypesGen.Workspace; + buildParameters?: TypesGen.WorkspaceBuildParameter[]; +}>; + +export type DeleteWorkspaceOptions = Pick< + TypesGen.CreateWorkspaceBuildRequest, + 'log_level' & 'orphan' +>; + +type Claims = { + license_expires: number; + account_type?: string; + account_id?: string; + trial: boolean; + all_features: boolean; + version: number; + features: Record; + require_telemetry?: boolean; +}; + +export type GetLicensesResponse = Omit & { + claims: Claims; + expires_at: string; +}; + +export type InsightsParams = { + start_time: string; + end_time: string; + template_ids: string; +}; + +export type InsightsTemplateParams = InsightsParams & { + interval: 'day' | 'week'; +}; + +export type GetJFrogXRayScanParams = { + workspaceId: string; + agentId: string; +}; + +export class MissingBuildParameters extends Error { + parameters: TypesGen.TemplateVersionParameter[] = []; + versionId: string; + + constructor( + parameters: TypesGen.TemplateVersionParameter[], + versionId: string, + ) { + super('Missing build parameters.'); + this.parameters = parameters; + this.versionId = versionId; + } +} + +/** + * This is the container for all API methods. It's split off to make it more + * clear where API methods should go, but it is eventually merged into the Api + * class with a more flat hierarchy + * + * All public methods should be defined as arrow functions to ensure that they + * can be passed around the React UI without losing their `this` context. + * + * This is one of the few cases where you have to worry about the difference + * between traditional methods and arrow function properties. Arrow functions + * disable JS's dynamic scope, and force all `this` references to resolve via + * lexical scope. + */ +class ApiMethods { + constructor(protected readonly axios: AxiosInstance) {} + + login = async ( + email: string, + password: string, + ): Promise => { + const payload = JSON.stringify({ email, password }); + const response = await this.axios.post( + '/api/v2/users/login', + payload, + { headers: { ...BASE_CONTENT_TYPE_JSON } }, + ); + + return response.data; + }; + + convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => { + const response = await this.axios.post( + '/api/v2/users/me/convert-login', + request, + ); + + return response.data; + }; + + logout = async (): Promise => { + return this.axios.post('/api/v2/users/logout'); + }; + + getAuthenticatedUser = async () => { + const response = await this.axios.get('/api/v2/users/me'); + return response.data; + }; + + getUserParameters = async (templateID: string) => { + const response = await this.axios.get( + `/api/v2/users/me/autofill-parameters?template_id=${templateID}`, + ); + + return response.data; + }; + + getAuthMethods = async (): Promise => { + const response = await this.axios.get( + '/api/v2/users/authmethods', + ); + + return response.data; + }; + + getUserLoginType = async (): Promise => { + const response = await this.axios.get( + '/api/v2/users/me/login-type', + ); + + return response.data; + }; + + checkAuthorization = async ( + params: TypesGen.AuthorizationRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/authcheck`, + params, + ); + + return response.data; + }; + + getApiKey = async (): Promise => { + const response = await this.axios.post( + '/api/v2/users/me/keys', + ); + + return response.data; + }; + + getTokens = async ( + params: TypesGen.TokensFilter, + ): Promise => { + const response = await this.axios.get( + `/api/v2/users/me/keys/tokens`, + { params }, + ); + + return response.data; + }; + + deleteToken = async (keyId: string): Promise => { + await this.axios.delete(`/api/v2/users/me/keys/${keyId}`); + }; + + createToken = async ( + params: TypesGen.CreateTokenRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/users/me/keys/tokens`, + params, + ); + + return response.data; + }; + + getTokenConfig = async (): Promise => { + const response = await this.axios.get( + '/api/v2/users/me/keys/tokens/tokenconfig', + ); + + return response.data; + }; + + getUsers = async ( + options: TypesGen.UsersRequest, + signal?: AbortSignal, + ): Promise => { + const url = getURLWithSearchParams('/api/v2/users', options); + const response = await this.axios.get( + url.toString(), + { signal }, + ); + + return response.data; + }; + + getOrganization = async ( + organizationId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}`, + ); + + return response.data; + }; + + getOrganizations = async (): Promise => { + const response = await this.axios.get( + '/api/v2/users/me/organizations', + ); + return response.data; + }; + + getTemplate = async (templateId: string): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}`, + ); + + return response.data; + }; + + getTemplates = async ( + organizationId: string, + options?: TemplateOptions, + ): Promise => { + const params: Record = {}; + if (options?.deprecated !== undefined) { + // Just want to check if it isn't undefined. If it has + // a boolean value, convert it to a string and include + // it as a param. + params.deprecated = String(options.deprecated); + } + + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates`, + { params }, + ); + + return response.data; + }; + + getTemplateByName = async ( + organizationId: string, + name: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates/${name}`, + ); + + return response.data; + }; + + getTemplateVersion = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}`, + ); + + return response.data; + }; + + getTemplateVersionResources = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/resources`, + ); + + return response.data; + }; + + getTemplateVersionVariables = async ( + versionId: string, + ): Promise => { + // Defined as separate variable to avoid wonky Prettier formatting because + // the type definition is so long + type VerArray = TypesGen.TemplateVersionVariable[]; + + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/variables`, + ); + + return response.data; + }; + + getTemplateVersions = async ( + templateId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}/versions`, + ); + return response.data; + }; + + getTemplateVersionByName = async ( + organizationId: string, + templateName: string, + versionName: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`, + ); + + return response.data; + }; + + getPreviousTemplateVersionByName = async ( + organizationId: string, + templateName: string, + versionName: string, + ) => { + try { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}/previous`, + ); + + return response.data; + } catch (error) { + // When there is no previous version, like the first version of a + // template, the API returns 404 so in this case we can safely return + // undefined + const is404 = + isAxiosError(error) && error.response && error.response.status === 404; + + if (is404) { + return undefined; + } + + throw error; + } + }; + + createTemplateVersion = async ( + organizationId: string, + data: TypesGen.CreateTemplateVersionRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/templateversions`, + data, + ); + + return response.data; + }; + + getTemplateVersionExternalAuth = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/external-auth`, + ); + + return response.data; + }; + + getTemplateVersionRichParameters = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/rich-parameters`, + ); + return response.data; + }; + + createTemplate = async ( + organizationId: string, + data: TypesGen.CreateTemplateRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/templates`, + data, + ); + + return response.data; + }; + + updateActiveTemplateVersion = async ( + templateId: string, + data: TypesGen.UpdateActiveTemplateVersion, + ) => { + const response = await this.axios.patch( + `/api/v2/templates/${templateId}/versions`, + data, + ); + return response.data; + }; + + patchTemplateVersion = async ( + templateVersionId: string, + data: TypesGen.PatchTemplateVersionRequest, + ) => { + const response = await this.axios.patch( + `/api/v2/templateversions/${templateVersionId}`, + data, + ); + + return response.data; + }; + + archiveTemplateVersion = async (templateVersionId: string) => { + const response = await this.axios.post( + `/api/v2/templateversions/${templateVersionId}/archive`, + ); + + return response.data; + }; + + unarchiveTemplateVersion = async (templateVersionId: string) => { + const response = await this.axios.post( + `/api/v2/templateversions/${templateVersionId}/unarchive`, + ); + return response.data; + }; + + updateTemplateMeta = async ( + templateId: string, + data: TypesGen.UpdateTemplateMeta, + ): Promise => { + const response = await this.axios.patch( + `/api/v2/templates/${templateId}`, + data, + ); + + // On 304 response there is no data payload. + if (response.status === 304) { + return null; + } + + return response.data; + }; + + deleteTemplate = async (templateId: string): Promise => { + const response = await this.axios.delete( + `/api/v2/templates/${templateId}`, + ); + + return response.data; + }; + + getWorkspace = async ( + workspaceId: string, + params?: TypesGen.WorkspaceOptions, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaces/${workspaceId}`, + { params }, + ); + + return response.data; + }; + + getWorkspaces = async ( + options: TypesGen.WorkspacesRequest, + ): Promise => { + const url = getURLWithSearchParams('/api/v2/workspaces', options); + const response = await this.axios.get(url); + return response.data; + }; + + getWorkspaceByOwnerAndName = async ( + username = 'me', + workspaceName: string, + params?: TypesGen.WorkspaceOptions, + ): Promise => { + const response = await this.axios.get( + `/api/v2/users/${username}/workspace/${workspaceName}`, + { params }, + ); + + return response.data; + }; + + getWorkspaceBuildByNumber = async ( + username = 'me', + workspaceName: string, + buildNumber: number, + ): Promise => { + const response = await this.axios.get( + `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, + ); + + return response.data; + }; + + waitForBuild = (build: TypesGen.WorkspaceBuild) => { + return new Promise((res, reject) => { + void (async () => { + let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined; + + while ( + // eslint-disable-next-line no-loop-func -- Not great, but should be harmless + !['succeeded', 'canceled'].some(status => + latestJobInfo?.status.includes(status), + ) + ) { + const { job } = await this.getWorkspaceBuildByNumber( + build.workspace_owner_name, + build.workspace_name, + build.build_number, + ); + + latestJobInfo = job; + if (latestJobInfo.status === 'failed') { + return reject(latestJobInfo); + } + + await delay(1000); + } + + return res(latestJobInfo); + })(); + }); + }; + + postWorkspaceBuild = async ( + workspaceId: string, + data: TypesGen.CreateWorkspaceBuildRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/workspaces/${workspaceId}/builds`, + data, + ); + + return response.data; + }; + + startWorkspace = ( + workspaceId: string, + templateVersionId: string, + logLevel?: TypesGen.ProvisionerLogLevel, + buildParameters?: TypesGen.WorkspaceBuildParameter[], + ) => { + return this.postWorkspaceBuild(workspaceId, { + transition: 'start', + template_version_id: templateVersionId, + log_level: logLevel, + rich_parameter_values: buildParameters, + }); + }; + + stopWorkspace = ( + workspaceId: string, + logLevel?: TypesGen.ProvisionerLogLevel, + ) => { + return this.postWorkspaceBuild(workspaceId, { + transition: 'stop', + log_level: logLevel, + }); + }; + + deleteWorkspace = (workspaceId: string, options?: DeleteWorkspaceOptions) => { + return this.postWorkspaceBuild(workspaceId, { + transition: 'delete', + ...options, + }); + }; + + cancelWorkspaceBuild = async ( + workspaceBuildId: TypesGen.WorkspaceBuild['id'], + ): Promise => { + const response = await this.axios.patch( + `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`, + ); + + return response.data; + }; + + updateWorkspaceDormancy = async ( + workspaceId: string, + dormant: boolean, + ): Promise => { + const data: TypesGen.UpdateWorkspaceDormancy = { dormant }; + const response = await this.axios.put( + `/api/v2/workspaces/${workspaceId}/dormant`, + data, + ); + + return response.data; + }; + + updateWorkspaceAutomaticUpdates = async ( + workspaceId: string, + automaticUpdates: TypesGen.AutomaticUpdates, + ): Promise => { + const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = { + automatic_updates: automaticUpdates, + }; + + const response = await this.axios.put( + `/api/v2/workspaces/${workspaceId}/autoupdates`, + req, + ); + + return response.data; + }; + + restartWorkspace = async ({ + workspace, + buildParameters, + }: RestartWorkspaceParameters): Promise => { + const stopBuild = await this.stopWorkspace(workspace.id); + const awaitedStopBuild = await this.waitForBuild(stopBuild); + + // If the restart is canceled halfway through, make sure we bail + if (awaitedStopBuild?.status === 'canceled') { + return; + } + + const startBuild = await this.startWorkspace( + workspace.id, + workspace.latest_build.template_version_id, + undefined, + buildParameters, + ); + + await this.waitForBuild(startBuild); + }; + + cancelTemplateVersionBuild = async ( + templateVersionId: TypesGen.TemplateVersion['id'], + ): Promise => { + const response = await this.axios.patch( + `/api/v2/templateversions/${templateVersionId}/cancel`, + ); + + return response.data; + }; + + createUser = async ( + user: TypesGen.CreateUserRequest, + ): Promise => { + const response = await this.axios.post( + '/api/v2/users', + user, + ); + + return response.data; + }; + + createWorkspace = async ( + organizationId: string, + userId = 'me', + workspace: TypesGen.CreateWorkspaceRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`, + workspace, + ); + + return response.data; + }; + + patchWorkspace = async ( + workspaceId: string, + data: TypesGen.UpdateWorkspaceRequest, + ): Promise => { + await this.axios.patch(`/api/v2/workspaces/${workspaceId}`, data); + }; + + getBuildInfo = async (): Promise => { + const response = await this.axios.get('/api/v2/buildinfo'); + return response.data; + }; + + getUpdateCheck = async (): Promise => { + const response = await this.axios.get('/api/v2/updatecheck'); + return response.data; + }; + + putWorkspaceAutostart = async ( + workspaceID: string, + autostart: TypesGen.UpdateWorkspaceAutostartRequest, + ): Promise => { + const payload = JSON.stringify(autostart); + await this.axios.put( + `/api/v2/workspaces/${workspaceID}/autostart`, + payload, + { headers: { ...BASE_CONTENT_TYPE_JSON } }, + ); + }; + + putWorkspaceAutostop = async ( + workspaceID: string, + ttl: TypesGen.UpdateWorkspaceTTLRequest, + ): Promise => { + const payload = JSON.stringify(ttl); + await this.axios.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, { + headers: { ...BASE_CONTENT_TYPE_JSON }, + }); + }; + + updateProfile = async ( + userId: string, + data: TypesGen.UpdateUserProfileRequest, + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/profile`, + data, + ); + return response.data; + }; + + updateAppearanceSettings = async ( + userId: string, + data: TypesGen.UpdateUserAppearanceSettingsRequest, + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/appearance`, + data, + ); + return response.data; + }; + + getUserQuietHoursSchedule = async ( + userId: TypesGen.User['id'], + ): Promise => { + const response = await this.axios.get( + `/api/v2/users/${userId}/quiet-hours`, + ); + return response.data; + }; + + updateUserQuietHoursSchedule = async ( + userId: TypesGen.User['id'], + data: TypesGen.UpdateUserQuietHoursScheduleRequest, + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/quiet-hours`, + data, + ); + + return response.data; + }; + + activateUser = async ( + userId: TypesGen.User['id'], + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/status/activate`, + ); + return response.data; + }; + + suspendUser = async (userId: TypesGen.User['id']): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/status/suspend`, + ); + + return response.data; + }; + + deleteUser = async (userId: TypesGen.User['id']): Promise => { + await this.axios.delete(`/api/v2/users/${userId}`); + }; + + // API definition: + // https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53 + hasFirstUser = async (): Promise => { + try { + // If it is success, it is true + await this.axios.get('/api/v2/users/first'); + return true; + } catch (error) { + // If it returns a 404, it is false + if (isAxiosError(error) && error.response?.status === 404) { + return false; + } + + throw error; + } + }; + + createFirstUser = async ( + req: TypesGen.CreateFirstUserRequest, + ): Promise => { + const response = await this.axios.post(`/api/v2/users/first`, req); + return response.data; + }; + + updateUserPassword = async ( + userId: TypesGen.User['id'], + updatePassword: TypesGen.UpdateUserPasswordRequest, + ): Promise => { + await this.axios.put(`/api/v2/users/${userId}/password`, updatePassword); + }; + + getRoles = async (): Promise> => { + const response = await this.axios.get( + `/api/v2/users/roles`, + ); + + return response.data; + }; + + updateUserRoles = async ( + roles: TypesGen.SlimRole['name'][], + userId: TypesGen.User['id'], + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/roles`, + { roles }, + ); + + return response.data; + }; + + getUserSSHKey = async (userId = 'me'): Promise => { + const response = await this.axios.get( + `/api/v2/users/${userId}/gitsshkey`, + ); + + return response.data; + }; + + regenerateUserSSHKey = async (userId = 'me'): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/gitsshkey`, + ); + + return response.data; + }; + + getWorkspaceBuilds = async ( + workspaceId: string, + req?: TypesGen.WorkspaceBuildsRequest, + ) => { + const response = await this.axios.get( + getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req), + ); + + return response.data; + }; + + getWorkspaceBuildLogs = async ( + buildId: string, + before: Date, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspacebuilds/${buildId}/logs?before=${before.getTime()}`, + ); + + return response.data; + }; + + getWorkspaceAgentLogs = async ( + agentID: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaceagents/${agentID}/logs`, + ); + + return response.data; + }; + + putWorkspaceExtension = async ( + workspaceId: string, + newDeadline: dayjs.Dayjs, + ): Promise => { + await this.axios.put(`/api/v2/workspaces/${workspaceId}/extend`, { + deadline: newDeadline, + }); + }; + + refreshEntitlements = async (): Promise => { + await this.axios.post('/api/v2/licenses/refresh-entitlements'); + }; + + getEntitlements = async (): Promise => { + try { + const response = await this.axios.get( + '/api/v2/entitlements', + ); + + return response.data; + } catch (ex) { + if (isAxiosError(ex) && ex.response?.status === 404) { + return { + errors: [], + features: withDefaultFeatures({}), + has_license: false, + require_telemetry: false, + trial: false, + warnings: [], + refreshed_at: '', + }; + } + throw ex; + } + }; + + getExperiments = async (): Promise => { + try { + const response = await this.axios.get( + '/api/v2/experiments', + ); + + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return []; + } + + throw error; + } + }; + + getAvailableExperiments = + async (): Promise => { + try { + const response = await this.axios.get('/api/v2/experiments/available'); + + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return { safe: [] }; + } + throw error; + } + }; + + getExternalAuthProvider = async ( + provider: string, + ): Promise => { + const res = await this.axios.get(`/api/v2/external-auth/${provider}`); + return res.data; + }; + + getExternalAuthDevice = async ( + provider: string, + ): Promise => { + const resp = await this.axios.get( + `/api/v2/external-auth/${provider}/device`, + ); + return resp.data; + }; + + exchangeExternalAuthDevice = async ( + provider: string, + req: TypesGen.ExternalAuthDeviceExchange, + ): Promise => { + const resp = await this.axios.post( + `/api/v2/external-auth/${provider}/device`, + req, + ); + + return resp.data; + }; + + getUserExternalAuthProviders = + async (): Promise => { + const resp = await this.axios.get(`/api/v2/external-auth`); + return resp.data; + }; + + unlinkExternalAuthProvider = async (provider: string): Promise => { + const resp = await this.axios.delete(`/api/v2/external-auth/${provider}`); + return resp.data; + }; + + getOAuth2ProviderApps = async ( + filter?: TypesGen.OAuth2ProviderAppFilter, + ): Promise => { + const params = filter?.user_id + ? new URLSearchParams({ user_id: filter.user_id }).toString() + : ''; + + const resp = await this.axios.get(`/api/v2/oauth2-provider/apps?${params}`); + return resp.data; + }; + + getOAuth2ProviderApp = async ( + id: string, + ): Promise => { + const resp = await this.axios.get(`/api/v2/oauth2-provider/apps/${id}`); + return resp.data; + }; + + postOAuth2ProviderApp = async ( + data: TypesGen.PostOAuth2ProviderAppRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/oauth2-provider/apps`, + data, + ); + return response.data; + }; + + putOAuth2ProviderApp = async ( + id: string, + data: TypesGen.PutOAuth2ProviderAppRequest, + ): Promise => { + const response = await this.axios.put( + `/api/v2/oauth2-provider/apps/${id}`, + data, + ); + return response.data; + }; + + deleteOAuth2ProviderApp = async (id: string): Promise => { + await this.axios.delete(`/api/v2/oauth2-provider/apps/${id}`); + }; + + getOAuth2ProviderAppSecrets = async ( + id: string, + ): Promise => { + const resp = await this.axios.get( + `/api/v2/oauth2-provider/apps/${id}/secrets`, + ); + return resp.data; + }; + + postOAuth2ProviderAppSecret = async ( + id: string, + ): Promise => { + const resp = await this.axios.post( + `/api/v2/oauth2-provider/apps/${id}/secrets`, + ); + return resp.data; + }; + + deleteOAuth2ProviderAppSecret = async ( + appId: string, + secretId: string, + ): Promise => { + await this.axios.delete( + `/api/v2/oauth2-provider/apps/${appId}/secrets/${secretId}`, + ); + }; + + revokeOAuth2ProviderApp = async (appId: string): Promise => { + await this.axios.delete(`/oauth2/tokens?client_id=${appId}`); + }; + + getAuditLogs = async ( + options: TypesGen.AuditLogsRequest, + ): Promise => { + const url = getURLWithSearchParams('/api/v2/audit', options); + const response = await this.axios.get(url); + return response.data; + }; + + getTemplateDAUs = async ( + templateId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}/daus`, + ); + + return response.data; + }; + + getDeploymentDAUs = async ( + // Default to user's local timezone. + // As /api/v2/insights/daus only accepts whole-number values for tz_offset + // we truncate the tz offset down to the closest hour. + offset = Math.trunc(new Date().getTimezoneOffset() / 60), + ): Promise => { + const response = await this.axios.get( + `/api/v2/insights/daus?tz_offset=${offset}`, + ); + + return response.data; + }; + + getTemplateACLAvailable = async ( + templateId: string, + options: TypesGen.UsersRequest, + ): Promise => { + const url = getURLWithSearchParams( + `/api/v2/templates/${templateId}/acl/available`, + options, + ).toString(); + + const response = await this.axios.get(url); + return response.data; + }; + + getTemplateACL = async ( + templateId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}/acl`, + ); + + return response.data; + }; + + updateTemplateACL = async ( + templateId: string, + data: TypesGen.UpdateTemplateACL, + ): Promise<{ message: string }> => { + const response = await this.axios.patch( + `/api/v2/templates/${templateId}/acl`, + data, + ); + + return response.data; + }; + + getApplicationsHost = async (): Promise => { + const response = await this.axios.get(`/api/v2/applications/host`); + return response.data; + }; + + getGroups = async (organizationId: string): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/groups`, + ); + + return response.data; + }; + + createGroup = async ( + organizationId: string, + data: TypesGen.CreateGroupRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/groups`, + data, + ); + return response.data; + }; + + getGroup = async (groupId: string): Promise => { + const response = await this.axios.get(`/api/v2/groups/${groupId}`); + return response.data; + }; + + patchGroup = async ( + groupId: string, + data: TypesGen.PatchGroupRequest, + ): Promise => { + const response = await this.axios.patch(`/api/v2/groups/${groupId}`, data); + return response.data; + }; + + addMember = async (groupId: string, userId: string) => { + return this.patchGroup(groupId, { + name: '', + add_users: [userId], + remove_users: [], + }); + }; + + removeMember = async (groupId: string, userId: string) => { + return this.patchGroup(groupId, { + name: '', + display_name: '', + add_users: [], + remove_users: [userId], + }); + }; + + deleteGroup = async (groupId: string): Promise => { + await this.axios.delete(`/api/v2/groups/${groupId}`); + }; + + getWorkspaceQuota = async ( + username: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspace-quota/${encodeURIComponent(username)}`, + ); + return response.data; + }; + + getAgentListeningPorts = async ( + agentID: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaceagents/${agentID}/listening-ports`, + ); + return response.data; + }; + + getWorkspaceAgentSharedPorts = async ( + workspaceID: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaces/${workspaceID}/port-share`, + ); + return response.data; + }; + + upsertWorkspaceAgentSharedPort = async ( + workspaceID: string, + req: TypesGen.UpsertWorkspaceAgentPortShareRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/workspaces/${workspaceID}/port-share`, + req, + ); + return response.data; + }; + + deleteWorkspaceAgentSharedPort = async ( + workspaceID: string, + req: TypesGen.DeleteWorkspaceAgentPortShareRequest, + ): Promise => { + const response = await this.axios.delete( + `/api/v2/workspaces/${workspaceID}/port-share`, + { data: req }, + ); + + return response.data; + }; + + // getDeploymentSSHConfig is used by the VSCode-Extension. + getDeploymentSSHConfig = async (): Promise => { + const response = await this.axios.get(`/api/v2/deployment/ssh`); + return response.data; + }; + + getDeploymentConfig = async (): Promise => { + const response = await this.axios.get(`/api/v2/deployment/config`); + return response.data; + }; + + getDeploymentStats = async (): Promise => { + const response = await this.axios.get(`/api/v2/deployment/stats`); + return response.data; + }; + + getReplicas = async (): Promise => { + const response = await this.axios.get(`/api/v2/replicas`); + return response.data; + }; + + getFile = async (fileId: string): Promise => { + const response = await this.axios.get( + `/api/v2/files/${fileId}`, + { responseType: 'arraybuffer' }, + ); + + return response.data; + }; + + getWorkspaceProxyRegions = async (): Promise< + TypesGen.RegionsResponse + > => { + const response = await this.axios.get< + TypesGen.RegionsResponse + >(`/api/v2/regions`); + + return response.data; + }; + + getWorkspaceProxies = async (): Promise< + TypesGen.RegionsResponse + > => { + const response = await this.axios.get< + TypesGen.RegionsResponse + >(`/api/v2/workspaceproxies`); + + return response.data; + }; + + createWorkspaceProxy = async ( + b: TypesGen.CreateWorkspaceProxyRequest, + ): Promise => { + const response = await this.axios.post(`/api/v2/workspaceproxies`, b); + return response.data; + }; + + getAppearance = async (): Promise => { + try { + const response = await this.axios.get(`/api/v2/appearance`); + return response.data || {}; + } catch (ex) { + if (isAxiosError(ex) && ex.response?.status === 404) { + return { + application_name: '', + logo_url: '', + notification_banners: [], + service_banner: { + enabled: false, + }, + }; + } + + throw ex; + } + }; + + updateAppearance = async ( + b: TypesGen.AppearanceConfig, + ): Promise => { + const response = await this.axios.put(`/api/v2/appearance`, b); + return response.data; + }; + + getTemplateExamples = async ( + organizationId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates/examples`, + ); + + return response.data; + }; + + uploadFile = async (file: File): Promise => { + const response = await this.axios.post('/api/v2/files', file, { + headers: { 'Content-Type': 'application/x-tar' }, + }); + + return response.data; + }; + + getTemplateVersionLogs = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/logs`, + ); + return response.data; + }; + + updateWorkspaceVersion = async ( + workspace: TypesGen.Workspace, + ): Promise => { + const template = await this.getTemplate(workspace.template_id); + return this.startWorkspace(workspace.id, template.active_version_id); + }; + + getWorkspaceBuildParameters = async ( + workspaceBuildId: TypesGen.WorkspaceBuild['id'], + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspacebuilds/${workspaceBuildId}/parameters`, + ); + + return response.data; + }; + + getLicenses = async (): Promise => { + const response = await this.axios.get(`/api/v2/licenses`); + return response.data; + }; + + createLicense = async ( + data: TypesGen.AddLicenseRequest, + ): Promise => { + const response = await this.axios.post(`/api/v2/licenses`, data); + return response.data; + }; + + removeLicense = async (licenseId: number): Promise => { + await this.axios.delete(`/api/v2/licenses/${licenseId}`); + }; + + /** Steps to change the workspace version + * - Get the latest template to access the latest active version + * - Get the current build parameters + * - Get the template parameters + * - Update the build parameters and check if there are missed parameters for + * the new version + * - If there are missing parameters raise an error + * - Create a build with the version and updated build parameters + */ + changeWorkspaceVersion = async ( + workspace: TypesGen.Workspace, + templateVersionId: string, + newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], + ): Promise => { + const [currentBuildParameters, templateParameters] = await Promise.all([ + this.getWorkspaceBuildParameters(workspace.latest_build.id), + this.getTemplateVersionRichParameters(templateVersionId), + ]); + + const missingParameters = getMissingParameters( + currentBuildParameters, + newBuildParameters, + templateParameters, + ); + + if (missingParameters.length > 0) { + throw new MissingBuildParameters(missingParameters, templateVersionId); + } + + return this.postWorkspaceBuild(workspace.id, { + transition: 'start', + template_version_id: templateVersionId, + rich_parameter_values: newBuildParameters, + }); + }; + + /** Steps to update the workspace + * - Get the latest template to access the latest active version + * - Get the current build parameters + * - Get the template parameters + * - Update the build parameters and check if there are missed parameters for + * the newest version + * - If there are missing parameters raise an error + * - Create a build with the latest version and updated build parameters + */ + updateWorkspace = async ( + workspace: TypesGen.Workspace, + newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], + ): Promise => { + const [template, oldBuildParameters] = await Promise.all([ + this.getTemplate(workspace.template_id), + this.getWorkspaceBuildParameters(workspace.latest_build.id), + ]); + + const activeVersionId = template.active_version_id; + const templateParameters = await this.getTemplateVersionRichParameters( + activeVersionId, + ); + + const missingParameters = getMissingParameters( + oldBuildParameters, + newBuildParameters, + templateParameters, + ); + + if (missingParameters.length > 0) { + throw new MissingBuildParameters(missingParameters, activeVersionId); + } + + return this.postWorkspaceBuild(workspace.id, { + transition: 'start', + template_version_id: activeVersionId, + rich_parameter_values: newBuildParameters, + }); + }; + + getWorkspaceResolveAutostart = async ( + workspaceId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaces/${workspaceId}/resolve-autostart`, + ); + return response.data; + }; + + issueReconnectingPTYSignedToken = async ( + params: TypesGen.IssueReconnectingPTYSignedTokenRequest, + ): Promise => { + const response = await this.axios.post( + '/api/v2/applications/reconnecting-pty-signed-token', + params, + ); + + return response.data; + }; + + getWorkspaceParameters = async (workspace: TypesGen.Workspace) => { + const latestBuild = workspace.latest_build; + const [templateVersionRichParameters, buildParameters] = await Promise.all([ + this.getTemplateVersionRichParameters(latestBuild.template_version_id), + this.getWorkspaceBuildParameters(latestBuild.id), + ]); + + return { + templateVersionRichParameters, + buildParameters, + }; + }; + + getInsightsUserLatency = async ( + filters: InsightsParams, + ): Promise => { + const params = new URLSearchParams(filters); + const response = await this.axios.get( + `/api/v2/insights/user-latency?${params}`, + ); + + return response.data; + }; + + getInsightsUserActivity = async ( + filters: InsightsParams, + ): Promise => { + const params = new URLSearchParams(filters); + const response = await this.axios.get( + `/api/v2/insights/user-activity?${params}`, + ); + + return response.data; + }; + + getInsightsTemplate = async ( + params: InsightsTemplateParams, + ): Promise => { + const searchParams = new URLSearchParams(params); + const response = await this.axios.get( + `/api/v2/insights/templates?${searchParams}`, + ); + + return response.data; + }; + + getHealth = async (force: boolean = false) => { + const params = new URLSearchParams({ force: force.toString() }); + const response = await this.axios.get( + `/api/v2/debug/health?${params}`, + ); + return response.data; + }; + + getHealthSettings = async (): Promise => { + const res = await this.axios.get( + `/api/v2/debug/health/settings`, + ); + + return res.data; + }; + + updateHealthSettings = async (data: TypesGen.UpdateHealthSettings) => { + const response = await this.axios.put( + `/api/v2/debug/health/settings`, + data, + ); + + return response.data; + }; + + putFavoriteWorkspace = async (workspaceID: string) => { + await this.axios.put(`/api/v2/workspaces/${workspaceID}/favorite`); + }; + + deleteFavoriteWorkspace = async (workspaceID: string) => { + await this.axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`); + }; + + getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => { + const searchParams = new URLSearchParams({ + workspace_id: options.workspaceId, + agent_id: options.agentId, + }); + + try { + const res = await this.axios.get( + `/api/v2/integrations/jfrog/xray-scan?${searchParams}`, + ); + + return res.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + // react-query library does not allow undefined to be returned as a + // query result + return null; + } + + throw error; + } + }; +} + +// This is a hard coded CSRF token/cookie pair for local development. In prod, +// the GoLang webserver generates a random cookie with a new token for each +// document request. For local development, we don't use the Go webserver for +// static files, so this is the 'hack' to make local development work with +// remote apis. The CSRF cookie for this token is "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=" +const csrfToken = + 'KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A=='; + +// Always attach CSRF token to all requests. In puppeteer the document is +// undefined. In those cases, just do nothing. +const tokenMetadataElement = + typeof document !== 'undefined' + ? document.head.querySelector('meta[property="csrf-token"]') + : null; + +function getConfiguredAxiosInstance(): AxiosInstance { + const instance = globalAxios.create(); + + // Adds 304 for the default axios validateStatus function + // https://github.com/axios/axios#handling-errors Check status here + // https://httpstatusdogs.com/ + instance.defaults.validateStatus = status => { + return (status >= 200 && status < 300) || status === 304; + }; + + const metadataIsAvailable = + tokenMetadataElement !== null && + tokenMetadataElement.getAttribute('content') !== null; + + if (metadataIsAvailable) { + if (process.env.NODE_ENV === 'development') { + // Development mode uses a hard-coded CSRF token + instance.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken; + instance.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken; + tokenMetadataElement.setAttribute('content', csrfToken); + } else { + instance.defaults.headers.common['X-CSRF-TOKEN'] = + tokenMetadataElement.getAttribute('content') ?? ''; + } + } else { + // Do not write error logs if we are in a FE unit test. + if (process.env.JEST_WORKER_ID === undefined) { + // eslint-disable-next-line no-console -- Function should never run in vendored version of SDK + console.error('CSRF token not found'); + } + } + + return instance; +} + +// Other non-API methods defined here to make it a little easier to find them. +interface ClientApi extends ApiMethods { + getCsrfToken: () => string; + setSessionToken: (token: string) => void; + setHost: (host: string | undefined) => void; + getAxiosInstance: () => AxiosInstance; +} + +export class Api extends ApiMethods implements ClientApi { + constructor() { + const scopedAxiosInstance = getConfiguredAxiosInstance(); + super(scopedAxiosInstance); + } + + // As with ApiMethods, all public methods should be defined with arrow + // function syntax to ensure they can be passed around the React UI without + // losing/detaching their `this` context! + + getCsrfToken = (): string => { + return csrfToken; + }; + + setSessionToken = (token: string): void => { + this.axios.defaults.headers.common['Coder-Session-Token'] = token; + }; + + setHost = (host: string | undefined): void => { + this.axios.defaults.baseURL = host; + }; + + getAxiosInstance = (): AxiosInstance => { + return this.axios; + }; +} + +export const API = new Api(); diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/errors.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/errors.ts new file mode 100644 index 00000000..6d401a11 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/errors.ts @@ -0,0 +1,124 @@ +import { type AxiosError, type AxiosResponse, isAxiosError } from 'axios'; + +const Language = { + errorsByCode: { + defaultErrorCode: 'Invalid value', + }, +}; + +export interface FieldError { + field: string; + detail: string; +} + +export type FieldErrors = Record; + +export interface ApiErrorResponse { + message: string; + detail?: string; + validations?: FieldError[]; +} + +export type ApiError = AxiosError & { + response: AxiosResponse; +}; + +export const isApiErrorResponse = (err: unknown): err is ApiErrorResponse => { + return ( + typeof err === 'object' && + err !== null && + 'message' in err && + typeof err.message === 'string' && + (!('detail' in err) || + err.detail === undefined || + typeof err.detail === 'string') && + (!('validations' in err) || + err.validations === undefined || + Array.isArray(err.validations)) + ); +}; + +export const isApiError = (err: unknown): err is ApiError => { + return ( + isAxiosError(err) && + err.response !== undefined && + isApiErrorResponse(err.response.data) + ); +}; + +export const hasApiFieldErrors = (error: ApiError): boolean => + Array.isArray(error.response.data.validations); + +export const isApiValidationError = (error: unknown): error is ApiError => { + return isApiError(error) && hasApiFieldErrors(error); +}; + +export const hasError = (error: unknown) => + error !== undefined && error !== null; + +export const mapApiErrorToFieldErrors = ( + apiErrorResponse: ApiErrorResponse, +): FieldErrors => { + const result: FieldErrors = {}; + + if (apiErrorResponse.validations) { + for (const error of apiErrorResponse.validations) { + result[error.field] = + error.detail || Language.errorsByCode.defaultErrorCode; + } + } + + return result; +}; + +/** + * + * @param error + * @param defaultMessage + * @returns error's message if ApiError or Error, else defaultMessage + */ +export const getErrorMessage = ( + error: unknown, + defaultMessage: string, +): string => { + // if error is API error + // 404s result in the default message being returned + if (isApiError(error) && error.response.data.message) { + return error.response.data.message; + } + if (isApiErrorResponse(error)) { + return error.message; + } + // if error is a non-empty string + if (error && typeof error === 'string') { + return error; + } + return defaultMessage; +}; + +/** + * + * @param error + * @returns a combined validation error message if the error is an ApiError + * and contains validation messages for different form fields. + */ +export const getValidationErrorMessage = (error: unknown): string => { + const validationErrors = + isApiError(error) && error.response.data.validations + ? error.response.data.validations + : []; + return validationErrors.map(error => error.detail).join('\n'); +}; + +export const getErrorDetail = (error: unknown): string | undefined | null => { + if (error instanceof Error) { + return 'Please check the developer console for more details.'; + } + if (isApiError(error)) { + return error.response.data.detail; + } + if (isApiErrorResponse(error)) { + return error.detail; + } + return null; +}; diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/typesGenerated.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/typesGenerated.ts new file mode 100644 index 00000000..2e3b4f04 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/typesGenerated.ts @@ -0,0 +1,2599 @@ +// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. + +// The code below is generated from codersdk. + +// From codersdk/templates.go +export interface ACLAvailable { + readonly users: readonly ReducedUser[]; + readonly groups: readonly Group[]; +} + +// From codersdk/apikey.go +export interface APIKey { + readonly id: string; + readonly user_id: string; + readonly last_used: string; + readonly expires_at: string; + readonly created_at: string; + readonly updated_at: string; + readonly login_type: LoginType; + readonly scope: APIKeyScope; + readonly token_name: string; + readonly lifetime_seconds: number; +} + +// From codersdk/apikey.go +export interface APIKeyWithOwner extends APIKey { + readonly username: string; +} + +// From codersdk/licenses.go +export interface AddLicenseRequest { + readonly license: string; +} + +// From codersdk/templates.go +export interface AgentStatsReportResponse { + readonly num_comms: number; + readonly rx_bytes: number; + readonly tx_bytes: number; +} + +// From codersdk/deployment.go +export interface AppHostResponse { + readonly host: string; +} + +// From codersdk/deployment.go +export interface AppearanceConfig { + readonly application_name: string; + readonly logo_url: string; + readonly service_banner: BannerConfig; + readonly notification_banners: readonly BannerConfig[]; + readonly support_links?: readonly LinkConfig[]; +} + +// From codersdk/templates.go +export interface ArchiveTemplateVersionsRequest { + readonly all: boolean; +} + +// From codersdk/templates.go +export interface ArchiveTemplateVersionsResponse { + readonly template_id: string; + readonly archived_ids: readonly string[]; +} + +// From codersdk/roles.go +export interface AssignableRoles extends Role { + readonly assignable: boolean; + readonly built_in: boolean; +} + +// From codersdk/audit.go +export type AuditDiff = Record; + +// From codersdk/audit.go +export interface AuditDiffField { + // Empty interface{} type, cannot resolve the type. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} + readonly old?: any; + // Empty interface{} type, cannot resolve the type. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} + readonly new?: any; + readonly secret: boolean; +} + +// From codersdk/audit.go +export interface AuditLog { + readonly id: string; + readonly request_id: string; + readonly time: string; + readonly organization_id: string; + // Named type "net/netip.Addr" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly ip: any; + readonly user_agent: string; + readonly resource_type: ResourceType; + readonly resource_id: string; + readonly resource_target: string; + readonly resource_icon: string; + readonly action: AuditAction; + readonly diff: AuditDiff; + readonly status_code: number; + readonly additional_fields: Record; + readonly description: string; + readonly resource_link: string; + readonly is_deleted: boolean; + readonly user?: User; +} + +// From codersdk/audit.go +export interface AuditLogResponse { + readonly audit_logs: readonly AuditLog[]; + readonly count: number; +} + +// From codersdk/audit.go +export interface AuditLogsRequest extends Pagination { + readonly q?: string; +} + +// From codersdk/users.go +export interface AuthMethod { + readonly enabled: boolean; +} + +// From codersdk/users.go +export interface AuthMethods { + readonly terms_of_service_url?: string; + readonly password: AuthMethod; + readonly github: AuthMethod; + readonly oidc: OIDCAuthMethod; +} + +// From codersdk/authorization.go +export interface AuthorizationCheck { + readonly object: AuthorizationObject; + readonly action: RBACAction; +} + +// From codersdk/authorization.go +export interface AuthorizationObject { + readonly resource_type: RBACResource; + readonly owner_id?: string; + readonly organization_id?: string; + readonly resource_id?: string; +} + +// From codersdk/authorization.go +export interface AuthorizationRequest { + readonly checks: Record; +} + +// From codersdk/authorization.go +export type AuthorizationResponse = Record; + +// From codersdk/deployment.go +export interface AvailableExperiments { + readonly safe: readonly Experiment[]; +} + +// From codersdk/deployment.go +export interface BannerConfig { + readonly enabled: boolean; + readonly message?: string; + readonly background_color?: string; +} + +// From codersdk/deployment.go +export interface BuildInfoResponse { + readonly external_url: string; + readonly version: string; + readonly dashboard_url: string; + readonly workspace_proxy: boolean; + readonly agent_api_version: string; + readonly upgrade_message: string; + readonly deployment_id: string; +} + +// From codersdk/insights.go +export interface ConnectionLatency { + readonly p50: number; + readonly p95: number; +} + +// From codersdk/users.go +export interface ConvertLoginRequest { + readonly to_type: LoginType; + readonly password: string; +} + +// From codersdk/users.go +export interface CreateFirstUserRequest { + readonly email: string; + readonly username: string; + readonly password: string; + readonly trial: boolean; + readonly trial_info: CreateFirstUserTrialInfo; +} + +// From codersdk/users.go +export interface CreateFirstUserResponse { + readonly user_id: string; + readonly organization_id: string; +} + +// From codersdk/users.go +export interface CreateFirstUserTrialInfo { + readonly first_name: string; + readonly last_name: string; + readonly phone_number: string; + readonly job_title: string; + readonly company_name: string; + readonly country: string; + readonly developers: string; +} + +// From codersdk/groups.go +export interface CreateGroupRequest { + readonly name: string; + readonly display_name: string; + readonly avatar_url: string; + readonly quota_allowance: number; +} + +// From codersdk/organizations.go +export interface CreateOrganizationRequest { + readonly name: string; +} + +// From codersdk/organizations.go +export interface CreateTemplateRequest { + readonly name: string; + readonly display_name?: string; + readonly description?: string; + readonly icon?: string; + readonly template_version_id: string; + readonly default_ttl_ms?: number; + readonly activity_bump_ms?: number; + readonly autostop_requirement?: TemplateAutostopRequirement; + readonly autostart_requirement?: TemplateAutostartRequirement; + readonly allow_user_cancel_workspace_jobs?: boolean; + readonly allow_user_autostart?: boolean; + readonly allow_user_autostop?: boolean; + readonly failure_ttl_ms?: number; + readonly dormant_ttl_ms?: number; + readonly delete_ttl_ms?: number; + readonly disable_everyone_group_access: boolean; + readonly require_active_version: boolean; +} + +// From codersdk/templateversions.go +export interface CreateTemplateVersionDryRunRequest { + readonly workspace_name: string; + readonly rich_parameter_values: readonly WorkspaceBuildParameter[]; + readonly user_variable_values?: readonly VariableValue[]; +} + +// From codersdk/organizations.go +export interface CreateTemplateVersionRequest { + readonly name?: string; + readonly message?: string; + readonly template_id?: string; + readonly storage_method: ProvisionerStorageMethod; + readonly file_id?: string; + readonly example_id?: string; + readonly provisioner: ProvisionerType; + readonly tags: Record; + readonly user_variable_values?: readonly VariableValue[]; +} + +// From codersdk/audit.go +export interface CreateTestAuditLogRequest { + readonly action?: AuditAction; + readonly resource_type?: ResourceType; + readonly resource_id?: string; + readonly additional_fields?: Record; + readonly time?: string; + readonly build_reason?: BuildReason; +} + +// From codersdk/apikey.go +export interface CreateTokenRequest { + readonly lifetime: number; + readonly scope: APIKeyScope; + readonly token_name: string; +} + +// From codersdk/users.go +export interface CreateUserRequest { + readonly email: string; + readonly username: string; + readonly password: string; + readonly login_type: LoginType; + readonly disable_login: boolean; + readonly organization_id: string; +} + +// From codersdk/workspaces.go +export interface CreateWorkspaceBuildRequest { + readonly template_version_id?: string; + readonly transition: WorkspaceTransition; + readonly dry_run?: boolean; + readonly state?: string; + readonly orphan?: boolean; + readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; + readonly log_level?: ProvisionerLogLevel; +} + +// From codersdk/workspaceproxy.go +export interface CreateWorkspaceProxyRequest { + readonly name: string; + readonly display_name: string; + readonly icon: string; +} + +// From codersdk/organizations.go +export interface CreateWorkspaceRequest { + readonly template_id?: string; + readonly template_version_id?: string; + readonly name: string; + readonly autostart_schedule?: string; + readonly ttl_ms?: number; + readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; + readonly automatic_updates?: AutomaticUpdates; +} + +// From codersdk/deployment.go +export interface DAUEntry { + readonly date: string; + readonly amount: number; +} + +// From codersdk/deployment.go +export interface DAURequest { + readonly TZHourOffset: number; +} + +// From codersdk/deployment.go +export interface DAUsResponse { + readonly entries: readonly DAUEntry[]; + readonly tz_hour_offset: number; +} + +// From codersdk/deployment.go +export interface DERP { + readonly server: DERPServerConfig; + readonly config: DERPConfig; +} + +// From codersdk/deployment.go +export interface DERPConfig { + readonly block_direct: boolean; + readonly force_websockets: boolean; + readonly url: string; + readonly path: string; +} + +// From codersdk/workspaceagents.go +export interface DERPRegion { + readonly preferred: boolean; + readonly latency_ms: number; +} + +// From codersdk/deployment.go +export interface DERPServerConfig { + readonly enable: boolean; + readonly region_id: number; + readonly region_code: string; + readonly region_name: string; + readonly stun_addresses: string[]; + readonly relay_url: string; +} + +// From codersdk/deployment.go +export interface DangerousConfig { + readonly allow_path_app_sharing: boolean; + readonly allow_path_app_site_owner_access: boolean; + readonly allow_all_cors: boolean; +} + +// From codersdk/workspaceagentportshare.go +export interface DeleteWorkspaceAgentPortShareRequest { + readonly agent_name: string; + readonly port: number; +} + +// From codersdk/deployment.go +export interface DeploymentConfig { + readonly config?: DeploymentValues; + readonly options?: SerpentOptionSet; +} + +// From codersdk/deployment.go +export interface DeploymentStats { + readonly aggregated_from: string; + readonly collected_at: string; + readonly next_update_at: string; + readonly workspaces: WorkspaceDeploymentStats; + readonly session_count: SessionCountDeploymentStats; +} + +// From codersdk/deployment.go +export interface DeploymentValues { + readonly verbose?: boolean; + readonly access_url?: string; + readonly wildcard_access_url?: string; + readonly docs_url?: string; + readonly redirect_to_access_url?: boolean; + readonly http_address?: string; + readonly autobuild_poll_interval?: number; + readonly job_hang_detector_interval?: number; + readonly derp?: DERP; + readonly prometheus?: PrometheusConfig; + readonly pprof?: PprofConfig; + readonly proxy_trusted_headers?: string[]; + readonly proxy_trusted_origins?: string[]; + readonly cache_directory?: string; + readonly in_memory_database?: boolean; + readonly pg_connection_url?: string; + readonly pg_auth?: string; + readonly oauth2?: OAuth2Config; + readonly oidc?: OIDCConfig; + readonly telemetry?: TelemetryConfig; + readonly tls?: TLSConfig; + readonly trace?: TraceConfig; + readonly secure_auth_cookie?: boolean; + readonly strict_transport_security?: number; + readonly strict_transport_security_options?: string[]; + readonly ssh_keygen_algorithm?: string; + readonly metrics_cache_refresh_interval?: number; + readonly agent_stat_refresh_interval?: number; + readonly agent_fallback_troubleshooting_url?: string; + readonly browser_only?: boolean; + readonly scim_api_key?: string; + readonly external_token_encryption_keys?: string[]; + readonly provisioner?: ProvisionerConfig; + readonly rate_limit?: RateLimitConfig; + readonly experiments?: string[]; + readonly update_check?: boolean; + readonly swagger?: SwaggerConfig; + readonly logging?: LoggingConfig; + readonly dangerous?: DangerousConfig; + readonly disable_path_apps?: boolean; + readonly session_lifetime?: SessionLifetime; + readonly disable_password_auth?: boolean; + readonly support?: SupportConfig; + readonly external_auth?: readonly ExternalAuthConfig[]; + readonly config_ssh?: SSHConfig; + readonly wgtunnel_host?: string; + readonly disable_owner_workspace_exec?: boolean; + readonly proxy_health_status_interval?: number; + readonly enable_terraform_debug_mode?: boolean; + readonly user_quiet_hours_schedule?: UserQuietHoursScheduleConfig; + readonly web_terminal_renderer?: string; + readonly allow_workspace_renames?: boolean; + readonly healthcheck?: HealthcheckConfig; + readonly cli_upgrade_message?: string; + readonly terms_of_service_url?: string; + readonly config?: string; + readonly write_config?: boolean; + readonly address?: string; +} + +// From codersdk/deployment.go +export interface Entitlements { + readonly features: Record; + readonly warnings: readonly string[]; + readonly errors: readonly string[]; + readonly has_license: boolean; + readonly trial: boolean; + readonly require_telemetry: boolean; + readonly refreshed_at: string; +} + +// From codersdk/deployment.go +export type Experiments = readonly Experiment[]; + +// From codersdk/externalauth.go +export interface ExternalAuth { + readonly authenticated: boolean; + readonly device: boolean; + readonly display_name: string; + readonly user?: ExternalAuthUser; + readonly app_installable: boolean; + readonly installations: readonly ExternalAuthAppInstallation[]; + readonly app_install_url: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthAppInstallation { + readonly id: number; + readonly account: ExternalAuthUser; + readonly configure_url: string; +} + +// From codersdk/deployment.go +export interface ExternalAuthConfig { + readonly type: string; + readonly client_id: string; + readonly id: string; + readonly auth_url: string; + readonly token_url: string; + readonly validate_url: string; + readonly app_install_url: string; + readonly app_installations_url: string; + readonly no_refresh: boolean; + readonly scopes: readonly string[]; + readonly extra_token_keys: readonly string[]; + readonly device_flow: boolean; + readonly device_code_url: string; + readonly regex: string; + readonly display_name: string; + readonly display_icon: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthDevice { + readonly device_code: string; + readonly user_code: string; + readonly verification_uri: string; + readonly expires_in: number; + readonly interval: number; +} + +// From codersdk/externalauth.go +export interface ExternalAuthDeviceExchange { + readonly device_code: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthLink { + readonly provider_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly has_refresh_token: boolean; + readonly expires: string; + readonly authenticated: boolean; + readonly validate_error: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthLinkProvider { + readonly id: string; + readonly type: string; + readonly device: boolean; + readonly display_name: string; + readonly display_icon: string; + readonly allow_refresh: boolean; + readonly allow_validate: boolean; +} + +// From codersdk/externalauth.go +export interface ExternalAuthUser { + readonly login: string; + readonly avatar_url: string; + readonly profile_url: string; + readonly name: string; +} + +// From codersdk/deployment.go +export interface Feature { + readonly entitlement: Entitlement; + readonly enabled: boolean; + readonly limit?: number; + readonly actual?: number; +} + +// From codersdk/apikey.go +export interface GenerateAPIKeyResponse { + readonly key: string; +} + +// From codersdk/users.go +export interface GetUsersResponse { + readonly users: readonly User[]; + readonly count: number; +} + +// From codersdk/gitsshkey.go +export interface GitSSHKey { + readonly user_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly public_key: string; +} + +// From codersdk/groups.go +export interface Group { + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly organization_id: string; + readonly members: readonly ReducedUser[]; + readonly avatar_url: string; + readonly quota_allowance: number; + readonly source: GroupSource; +} + +// From codersdk/workspaceapps.go +export interface Healthcheck { + readonly url: string; + readonly interval: number; + readonly threshold: number; +} + +// From codersdk/deployment.go +export interface HealthcheckConfig { + readonly refresh: number; + readonly threshold_database: number; +} + +// From codersdk/workspaceagents.go +export interface IssueReconnectingPTYSignedTokenRequest { + readonly url: string; + readonly agentID: string; +} + +// From codersdk/workspaceagents.go +export interface IssueReconnectingPTYSignedTokenResponse { + readonly signed_token: string; +} + +// From codersdk/jfrog.go +export interface JFrogXrayScan { + readonly workspace_id: string; + readonly agent_id: string; + readonly critical: number; + readonly high: number; + readonly medium: number; + readonly results_url: string; +} + +// From codersdk/licenses.go +export interface License { + readonly id: number; + readonly uuid: string; + readonly uploaded_at: string; + // Empty interface{} type, cannot resolve the type. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} + readonly claims: Record; +} + +// From codersdk/deployment.go +export interface LinkConfig { + readonly name: string; + readonly target: string; + readonly icon: string; +} + +// From codersdk/externalauth.go +export interface ListUserExternalAuthResponse { + readonly providers: readonly ExternalAuthLinkProvider[]; + readonly links: readonly ExternalAuthLink[]; +} + +// From codersdk/deployment.go +export interface LoggingConfig { + readonly log_filter: string[]; + readonly human: string; + readonly json: string; + readonly stackdriver: string; +} + +// From codersdk/users.go +export interface LoginWithPasswordRequest { + readonly email: string; + readonly password: string; +} + +// From codersdk/users.go +export interface LoginWithPasswordResponse { + readonly session_token: string; +} + +// From codersdk/users.go +export interface MinimalUser { + readonly id: string; + readonly username: string; + readonly avatar_url: string; +} + +// From codersdk/oauth2.go +export interface OAuth2AppEndpoints { + readonly authorization: string; + readonly token: string; + readonly device_authorization: string; +} + +// From codersdk/deployment.go +export interface OAuth2Config { + readonly github: OAuth2GithubConfig; +} + +// From codersdk/deployment.go +export interface OAuth2GithubConfig { + readonly client_id: string; + readonly client_secret: string; + readonly allowed_orgs: string[]; + readonly allowed_teams: string[]; + readonly allow_signups: boolean; + readonly allow_everyone: boolean; + readonly enterprise_base_url: string; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderApp { + readonly id: string; + readonly name: string; + readonly callback_url: string; + readonly icon: string; + readonly endpoints: OAuth2AppEndpoints; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderAppFilter { + readonly user_id?: string; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderAppSecret { + readonly id: string; + readonly last_used_at?: string; + readonly client_secret_truncated: string; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderAppSecretFull { + readonly id: string; + readonly client_secret_full: string; +} + +// From codersdk/users.go +export interface OAuthConversionResponse { + readonly state_string: string; + readonly expires_at: string; + readonly to_type: LoginType; + readonly user_id: string; +} + +// From codersdk/users.go +export interface OIDCAuthMethod extends AuthMethod { + readonly signInText: string; + readonly iconUrl: string; +} + +// From codersdk/deployment.go +export interface OIDCConfig { + readonly allow_signups: boolean; + readonly client_id: string; + readonly client_secret: string; + readonly client_key_file: string; + readonly client_cert_file: string; + readonly email_domain: string[]; + readonly issuer_url: string; + readonly scopes: string[]; + readonly ignore_email_verified: boolean; + readonly username_field: string; + readonly email_field: string; + readonly auth_url_params: Record; + readonly ignore_user_info: boolean; + readonly group_auto_create: boolean; + readonly group_regex_filter: string; + readonly group_allow_list: string[]; + readonly groups_field: string; + readonly group_mapping: Record; + readonly user_role_field: string; + readonly user_role_mapping: Record; + readonly user_roles_default: string[]; + readonly sign_in_text: string; + readonly icon_url: string; + readonly signups_disabled_text: string; +} + +// From codersdk/organizations.go +export interface Organization { + readonly id: string; + readonly name: string; + readonly created_at: string; + readonly updated_at: string; + readonly is_default: boolean; +} + +// From codersdk/organizations.go +export interface OrganizationMember { + readonly user_id: string; + readonly organization_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly roles: readonly SlimRole[]; +} + +// From codersdk/pagination.go +export interface Pagination { + readonly after_id?: string; + readonly limit?: number; + readonly offset?: number; +} + +// From codersdk/groups.go +export interface PatchGroupRequest { + readonly add_users: readonly string[]; + readonly remove_users: readonly string[]; + readonly name: string; + readonly display_name?: string; + readonly avatar_url?: string; + readonly quota_allowance?: number; +} + +// From codersdk/templateversions.go +export interface PatchTemplateVersionRequest { + readonly name: string; + readonly message?: string; +} + +// From codersdk/workspaceproxy.go +export interface PatchWorkspaceProxy { + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly icon: string; + readonly regenerate_token: boolean; +} + +// From codersdk/roles.go +export interface Permission { + readonly negate: boolean; + readonly resource_type: RBACResource; + readonly action: RBACAction; +} + +// From codersdk/oauth2.go +export interface PostOAuth2ProviderAppRequest { + readonly name: string; + readonly callback_url: string; + readonly icon: string; +} + +// From codersdk/deployment.go +export interface PprofConfig { + readonly enable: boolean; + readonly address: string; +} + +// From codersdk/deployment.go +export interface PrometheusConfig { + readonly enable: boolean; + readonly address: string; + readonly collect_agent_stats: boolean; + readonly collect_db_metrics: boolean; + readonly aggregate_agent_stats_by: string[]; +} + +// From codersdk/deployment.go +export interface ProvisionerConfig { + readonly daemons: number; + readonly daemon_types: string[]; + readonly daemon_poll_interval: number; + readonly daemon_poll_jitter: number; + readonly force_cancel_interval: number; + readonly daemon_psk: string; +} + +// From codersdk/provisionerdaemons.go +export interface ProvisionerDaemon { + readonly id: string; + readonly created_at: string; + readonly last_seen_at?: string; + readonly name: string; + readonly version: string; + readonly api_version: string; + readonly provisioners: readonly ProvisionerType[]; + readonly tags: Record; +} + +// From codersdk/provisionerdaemons.go +export interface ProvisionerJob { + readonly id: string; + readonly created_at: string; + readonly started_at?: string; + readonly completed_at?: string; + readonly canceled_at?: string; + readonly error?: string; + readonly error_code?: JobErrorCode; + readonly status: ProvisionerJobStatus; + readonly worker_id?: string; + readonly file_id: string; + readonly tags: Record; + readonly queue_position: number; + readonly queue_size: number; +} + +// From codersdk/provisionerdaemons.go +export interface ProvisionerJobLog { + readonly id: number; + readonly created_at: string; + readonly log_source: LogSource; + readonly log_level: LogLevel; + readonly stage: string; + readonly output: string; +} + +// From codersdk/workspaceproxy.go +export interface ProxyHealthReport { + readonly errors: readonly string[]; + readonly warnings: readonly string[]; +} + +// From codersdk/workspaces.go +export interface PutExtendWorkspaceRequest { + readonly deadline: string; +} + +// From codersdk/oauth2.go +export interface PutOAuth2ProviderAppRequest { + readonly name: string; + readonly callback_url: string; + readonly icon: string; +} + +// From codersdk/deployment.go +export interface RateLimitConfig { + readonly disable_all: boolean; + readonly api: number; +} + +// From codersdk/users.go +export interface ReducedUser extends MinimalUser { + readonly name: string; + readonly email: string; + readonly created_at: string; + readonly last_seen_at: string; + readonly status: UserStatus; + readonly login_type: LoginType; + readonly theme_preference: string; +} + +// From codersdk/workspaceproxy.go +export interface Region { + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly icon_url: string; + readonly healthy: boolean; + readonly path_app_url: string; + readonly wildcard_hostname: string; +} + +// From codersdk/workspaceproxy.go +export interface RegionsResponse { + readonly regions: readonly R[]; +} + +// From codersdk/replicas.go +export interface Replica { + readonly id: string; + readonly hostname: string; + readonly created_at: string; + readonly relay_address: string; + readonly region_id: number; + readonly error: string; + readonly database_latency: number; +} + +// From codersdk/workspaces.go +export interface ResolveAutostartResponse { + readonly parameter_mismatch: boolean; +} + +// From codersdk/client.go +export interface Response { + readonly message: string; + readonly detail?: string; + readonly validations?: readonly ValidationError[]; +} + +// From codersdk/roles.go +export interface Role { + readonly name: string; + readonly organization_id: string; + readonly display_name: string; + readonly site_permissions: readonly Permission[]; + readonly organization_permissions: Record; + readonly user_permissions: readonly Permission[]; +} + +// From codersdk/deployment.go +export interface SSHConfig { + readonly DeploymentName: string; + readonly SSHConfigOptions: string[]; +} + +// From codersdk/deployment.go +export interface SSHConfigResponse { + readonly hostname_prefix: string; + readonly ssh_config_options: Record; +} + +// From codersdk/serversentevents.go +export interface ServerSentEvent { + readonly type: ServerSentEventType; + // Empty interface{} type, cannot resolve the type. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} + readonly data: any; +} + +// From codersdk/deployment.go +export interface ServiceBannerConfig { + readonly enabled: boolean; + readonly message?: string; + readonly background_color?: string; +} + +// From codersdk/deployment.go +export interface SessionCountDeploymentStats { + readonly vscode: number; + readonly ssh: number; + readonly jetbrains: number; + readonly reconnecting_pty: number; +} + +// From codersdk/deployment.go +export interface SessionLifetime { + readonly disable_expiry_refresh?: boolean; + readonly default_duration: number; + readonly max_token_lifetime?: number; +} + +// From codersdk/roles.go +export interface SlimRole { + readonly name: string; + readonly display_name: string; +} + +// From codersdk/deployment.go +export interface SupportConfig { + readonly links: readonly LinkConfig[]; +} + +// From codersdk/deployment.go +export interface SwaggerConfig { + readonly enable: boolean; +} + +// From codersdk/deployment.go +export interface TLSConfig { + readonly enable: boolean; + readonly address: string; + readonly redirect_http: boolean; + readonly cert_file: string[]; + readonly client_auth: string; + readonly client_ca_file: string; + readonly key_file: string[]; + readonly min_version: string; + readonly client_cert_file: string; + readonly client_key_file: string; + readonly supported_ciphers: string[]; + readonly allow_insecure_ciphers: boolean; +} + +// From codersdk/deployment.go +export interface TelemetryConfig { + readonly enable: boolean; + readonly trace: boolean; + readonly url: string; +} + +// From codersdk/templates.go +export interface Template { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly organization_id: string; + readonly name: string; + readonly display_name: string; + readonly provisioner: ProvisionerType; + readonly active_version_id: string; + readonly active_user_count: number; + readonly build_time_stats: TemplateBuildTimeStats; + readonly description: string; + readonly deprecated: boolean; + readonly deprecation_message: string; + readonly icon: string; + readonly default_ttl_ms: number; + readonly activity_bump_ms: number; + readonly autostop_requirement: TemplateAutostopRequirement; + readonly autostart_requirement: TemplateAutostartRequirement; + readonly created_by_id: string; + readonly created_by_name: string; + readonly allow_user_autostart: boolean; + readonly allow_user_autostop: boolean; + readonly allow_user_cancel_workspace_jobs: boolean; + readonly failure_ttl_ms: number; + readonly time_til_dormant_ms: number; + readonly time_til_dormant_autodelete_ms: number; + readonly require_active_version: boolean; + readonly max_port_share_level: WorkspaceAgentPortShareLevel; +} + +// From codersdk/templates.go +export interface TemplateACL { + readonly users: readonly TemplateUser[]; + readonly group: readonly TemplateGroup[]; +} + +// From codersdk/insights.go +export interface TemplateAppUsage { + readonly template_ids: readonly string[]; + readonly type: TemplateAppsType; + readonly display_name: string; + readonly slug: string; + readonly icon: string; + readonly seconds: number; + readonly times_used: number; +} + +// From codersdk/templates.go +export interface TemplateAutostartRequirement { + readonly days_of_week: readonly string[]; +} + +// From codersdk/templates.go +export interface TemplateAutostopRequirement { + readonly days_of_week: readonly string[]; + readonly weeks: number; +} + +// From codersdk/templates.go +export type TemplateBuildTimeStats = Record< + WorkspaceTransition, + TransitionStats +>; + +// From codersdk/templates.go +export interface TemplateExample { + readonly id: string; + readonly url: string; + readonly name: string; + readonly description: string; + readonly icon: string; + readonly tags: readonly string[]; + readonly markdown: string; +} + +// From codersdk/templates.go +export interface TemplateGroup extends Group { + readonly role: TemplateRole; +} + +// From codersdk/insights.go +export interface TemplateInsightsIntervalReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; + readonly interval: InsightsReportInterval; + readonly active_users: number; +} + +// From codersdk/insights.go +export interface TemplateInsightsReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; + readonly active_users: number; + readonly apps_usage: readonly TemplateAppUsage[]; + readonly parameters_usage: readonly TemplateParameterUsage[]; +} + +// From codersdk/insights.go +export interface TemplateInsightsRequest { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; + readonly interval: InsightsReportInterval; + readonly sections: readonly TemplateInsightsSection[]; +} + +// From codersdk/insights.go +export interface TemplateInsightsResponse { + readonly report?: TemplateInsightsReport; + readonly interval_reports?: readonly TemplateInsightsIntervalReport[]; +} + +// From codersdk/insights.go +export interface TemplateParameterUsage { + readonly template_ids: readonly string[]; + readonly display_name: string; + readonly name: string; + readonly type: string; + readonly description: string; + readonly options?: readonly TemplateVersionParameterOption[]; + readonly values: readonly TemplateParameterValue[]; +} + +// From codersdk/insights.go +export interface TemplateParameterValue { + readonly value: string; + readonly count: number; +} + +// From codersdk/templates.go +export interface TemplateUser extends User { + readonly role: TemplateRole; +} + +// From codersdk/templateversions.go +export interface TemplateVersion { + readonly id: string; + readonly template_id?: string; + readonly organization_id?: string; + readonly created_at: string; + readonly updated_at: string; + readonly name: string; + readonly message: string; + readonly job: ProvisionerJob; + readonly readme: string; + readonly created_by: MinimalUser; + readonly archived: boolean; + readonly warnings?: readonly TemplateVersionWarning[]; +} + +// From codersdk/templateversions.go +export interface TemplateVersionExternalAuth { + readonly id: string; + readonly type: string; + readonly display_name: string; + readonly display_icon: string; + readonly authenticate_url: string; + readonly authenticated: boolean; + readonly optional?: boolean; +} + +// From codersdk/templateversions.go +export interface TemplateVersionParameter { + readonly name: string; + readonly display_name?: string; + readonly description: string; + readonly description_plaintext: string; + readonly type: string; + readonly mutable: boolean; + readonly default_value: string; + readonly icon: string; + readonly options: readonly TemplateVersionParameterOption[]; + readonly validation_error?: string; + readonly validation_regex?: string; + readonly validation_min?: number; + readonly validation_max?: number; + readonly validation_monotonic?: ValidationMonotonicOrder; + readonly required: boolean; + readonly ephemeral: boolean; +} + +// From codersdk/templateversions.go +export interface TemplateVersionParameterOption { + readonly name: string; + readonly description: string; + readonly value: string; + readonly icon: string; +} + +// From codersdk/templateversions.go +export interface TemplateVersionVariable { + readonly name: string; + readonly description: string; + readonly type: string; + readonly value: string; + readonly default_value: string; + readonly required: boolean; + readonly sensitive: boolean; +} + +// From codersdk/templates.go +export interface TemplateVersionsByTemplateRequest extends Pagination { + readonly template_id: string; + readonly include_archived: boolean; +} + +// From codersdk/apikey.go +export interface TokenConfig { + readonly max_token_lifetime: number; +} + +// From codersdk/apikey.go +export interface TokensFilter { + readonly include_all: boolean; +} + +// From codersdk/deployment.go +export interface TraceConfig { + readonly enable: boolean; + readonly honeycomb_api_key: string; + readonly capture_logs: boolean; + readonly data_dog: boolean; +} + +// From codersdk/templates.go +export interface TransitionStats { + readonly P50?: number; + readonly P95?: number; +} + +// From codersdk/templates.go +export interface UpdateActiveTemplateVersion { + readonly id: string; +} + +// From codersdk/deployment.go +export interface UpdateAppearanceConfig { + readonly application_name: string; + readonly logo_url: string; + readonly service_banner: BannerConfig; + readonly notification_banners: readonly BannerConfig[]; +} + +// From codersdk/updatecheck.go +export interface UpdateCheckResponse { + readonly current: boolean; + readonly version: string; + readonly url: string; +} + +// From codersdk/organizations.go +export interface UpdateOrganizationRequest { + readonly name: string; +} + +// From codersdk/users.go +export interface UpdateRoles { + readonly roles: readonly string[]; +} + +// From codersdk/templates.go +export interface UpdateTemplateACL { + readonly user_perms?: Record; + readonly group_perms?: Record; +} + +// From codersdk/templates.go +export interface UpdateTemplateMeta { + readonly name?: string; + readonly display_name?: string; + readonly description?: string; + readonly icon?: string; + readonly default_ttl_ms?: number; + readonly activity_bump_ms?: number; + readonly autostop_requirement?: TemplateAutostopRequirement; + readonly autostart_requirement?: TemplateAutostartRequirement; + readonly allow_user_autostart?: boolean; + readonly allow_user_autostop?: boolean; + readonly allow_user_cancel_workspace_jobs?: boolean; + readonly failure_ttl_ms?: number; + readonly time_til_dormant_ms?: number; + readonly time_til_dormant_autodelete_ms?: number; + readonly update_workspace_last_used_at: boolean; + readonly update_workspace_dormant_at: boolean; + readonly require_active_version?: boolean; + readonly deprecation_message?: string; + readonly disable_everyone_group_access: boolean; + readonly max_port_share_level?: WorkspaceAgentPortShareLevel; +} + +// From codersdk/users.go +export interface UpdateUserAppearanceSettingsRequest { + readonly theme_preference: string; +} + +// From codersdk/users.go +export interface UpdateUserPasswordRequest { + readonly old_password: string; + readonly password: string; +} + +// From codersdk/users.go +export interface UpdateUserProfileRequest { + readonly username: string; + readonly name: string; +} + +// From codersdk/users.go +export interface UpdateUserQuietHoursScheduleRequest { + readonly schedule: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceAutomaticUpdatesRequest { + readonly automatic_updates: AutomaticUpdates; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceAutostartRequest { + readonly schedule?: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceDormancy { + readonly dormant: boolean; +} + +// From codersdk/workspaceproxy.go +export interface UpdateWorkspaceProxyResponse { + readonly proxy: WorkspaceProxy; + readonly proxy_token: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceRequest { + readonly name?: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceTTLRequest { + readonly ttl_ms?: number; +} + +// From codersdk/files.go +export interface UploadResponse { + readonly hash: string; +} + +// From codersdk/workspaceagentportshare.go +export interface UpsertWorkspaceAgentPortShareRequest { + readonly agent_name: string; + readonly port: number; + readonly share_level: WorkspaceAgentPortShareLevel; + readonly protocol: WorkspaceAgentPortShareProtocol; +} + +// From codersdk/users.go +export interface User extends ReducedUser { + readonly organization_ids: readonly string[]; + readonly roles: readonly SlimRole[]; +} + +// From codersdk/insights.go +export interface UserActivity { + readonly template_ids: readonly string[]; + readonly user_id: string; + readonly username: string; + readonly avatar_url: string; + readonly seconds: number; +} + +// From codersdk/insights.go +export interface UserActivityInsightsReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; + readonly users: readonly UserActivity[]; +} + +// From codersdk/insights.go +export interface UserActivityInsightsRequest { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; +} + +// From codersdk/insights.go +export interface UserActivityInsightsResponse { + readonly report: UserActivityInsightsReport; +} + +// From codersdk/insights.go +export interface UserLatency { + readonly template_ids: readonly string[]; + readonly user_id: string; + readonly username: string; + readonly avatar_url: string; + readonly latency_ms: ConnectionLatency; +} + +// From codersdk/insights.go +export interface UserLatencyInsightsReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; + readonly users: readonly UserLatency[]; +} + +// From codersdk/insights.go +export interface UserLatencyInsightsRequest { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; +} + +// From codersdk/insights.go +export interface UserLatencyInsightsResponse { + readonly report: UserLatencyInsightsReport; +} + +// From codersdk/users.go +export interface UserLoginType { + readonly login_type: LoginType; +} + +// From codersdk/users.go +export interface UserParameter { + readonly name: string; + readonly value: string; +} + +// From codersdk/deployment.go +export interface UserQuietHoursScheduleConfig { + readonly default_schedule: string; + readonly allow_user_custom: boolean; +} + +// From codersdk/users.go +export interface UserQuietHoursScheduleResponse { + readonly raw_schedule: string; + readonly user_set: boolean; + readonly user_can_set: boolean; + readonly time: string; + readonly timezone: string; + readonly next: string; +} + +// From codersdk/users.go +export interface UserRoles { + readonly roles: readonly string[]; + readonly organization_roles: Record; +} + +// From codersdk/users.go +export interface UsersRequest extends Pagination { + readonly q?: string; +} + +// From codersdk/client.go +export interface ValidationError { + readonly field: string; + readonly detail: string; +} + +// From codersdk/organizations.go +export interface VariableValue { + readonly name: string; + readonly value: string; +} + +// From codersdk/workspaces.go +export interface Workspace { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly owner_id: string; + readonly owner_name: string; + readonly owner_avatar_url: string; + readonly organization_id: string; + readonly template_id: string; + readonly template_name: string; + readonly template_display_name: string; + readonly template_icon: string; + readonly template_allow_user_cancel_workspace_jobs: boolean; + readonly template_active_version_id: string; + readonly template_require_active_version: boolean; + readonly latest_build: WorkspaceBuild; + readonly outdated: boolean; + readonly name: string; + readonly autostart_schedule?: string; + readonly ttl_ms?: number; + readonly last_used_at: string; + readonly deleting_at?: string; + readonly dormant_at?: string; + readonly health: WorkspaceHealth; + readonly automatic_updates: AutomaticUpdates; + readonly allow_renames: boolean; + readonly favorite: boolean; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgent { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly first_connected_at?: string; + readonly last_connected_at?: string; + readonly disconnected_at?: string; + readonly started_at?: string; + readonly ready_at?: string; + readonly status: WorkspaceAgentStatus; + readonly lifecycle_state: WorkspaceAgentLifecycle; + readonly name: string; + readonly resource_id: string; + readonly instance_id?: string; + readonly architecture: string; + readonly environment_variables: Record; + readonly operating_system: string; + readonly logs_length: number; + readonly logs_overflowed: boolean; + readonly directory?: string; + readonly expanded_directory?: string; + readonly version: string; + readonly api_version: string; + readonly apps: readonly WorkspaceApp[]; + readonly latency?: Record; + readonly connection_timeout_seconds: number; + readonly troubleshooting_url: string; + readonly subsystems: readonly AgentSubsystem[]; + readonly health: WorkspaceAgentHealth; + readonly display_apps: readonly DisplayApp[]; + readonly log_sources: readonly WorkspaceAgentLogSource[]; + readonly scripts: readonly WorkspaceAgentScript[]; + readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentHealth { + readonly healthy: boolean; + readonly reason?: string; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentListeningPort { + readonly process_name: string; + readonly network: string; + readonly port: number; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentListeningPortsResponse { + readonly ports: readonly WorkspaceAgentListeningPort[]; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentLog { + readonly id: number; + readonly created_at: string; + readonly output: string; + readonly level: LogLevel; + readonly source_id: string; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentLogSource { + readonly workspace_agent_id: string; + readonly id: string; + readonly created_at: string; + readonly display_name: string; + readonly icon: string; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadata { + readonly result: WorkspaceAgentMetadataResult; + readonly description: WorkspaceAgentMetadataDescription; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadataDescription { + readonly display_name: string; + readonly key: string; + readonly script: string; + readonly interval: number; + readonly timeout: number; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadataResult { + readonly collected_at: string; + readonly age: number; + readonly value: string; + readonly error: string; +} + +// From codersdk/workspaceagentportshare.go +export interface WorkspaceAgentPortShare { + readonly workspace_id: string; + readonly agent_name: string; + readonly port: number; + readonly share_level: WorkspaceAgentPortShareLevel; + readonly protocol: WorkspaceAgentPortShareProtocol; +} + +// From codersdk/workspaceagentportshare.go +export interface WorkspaceAgentPortShares { + readonly shares: readonly WorkspaceAgentPortShare[]; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentScript { + readonly log_source_id: string; + readonly log_path: string; + readonly script: string; + readonly cron: string; + readonly run_on_start: boolean; + readonly run_on_stop: boolean; + readonly start_blocks_login: boolean; + readonly timeout: number; +} + +// From codersdk/workspaceapps.go +export interface WorkspaceApp { + readonly id: string; + readonly url: string; + readonly external: boolean; + readonly slug: string; + readonly display_name: string; + readonly command?: string; + readonly icon?: string; + readonly subdomain: boolean; + readonly subdomain_name?: string; + readonly sharing_level: WorkspaceAppSharingLevel; + readonly healthcheck: Healthcheck; + readonly health: WorkspaceAppHealth; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceBuild { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly workspace_id: string; + readonly workspace_name: string; + readonly workspace_owner_id: string; + readonly workspace_owner_name: string; + readonly workspace_owner_avatar_url: string; + readonly template_version_id: string; + readonly template_version_name: string; + readonly build_number: number; + readonly transition: WorkspaceTransition; + readonly initiator_id: string; + readonly initiator_name: string; + readonly job: ProvisionerJob; + readonly reason: BuildReason; + readonly resources: readonly WorkspaceResource[]; + readonly deadline?: string; + readonly max_deadline?: string; + readonly status: WorkspaceStatus; + readonly daily_cost: number; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceBuildParameter { + readonly name: string; + readonly value: string; +} + +// From codersdk/workspaces.go +export interface WorkspaceBuildsRequest extends Pagination { + readonly since?: string; +} + +// From codersdk/deployment.go +export interface WorkspaceConnectionLatencyMS { + readonly P50: number; + readonly P95: number; +} + +// From codersdk/deployment.go +export interface WorkspaceDeploymentStats { + readonly pending: number; + readonly building: number; + readonly running: number; + readonly failed: number; + readonly stopped: number; + readonly connection_latency_ms: WorkspaceConnectionLatencyMS; + readonly rx_bytes: number; + readonly tx_bytes: number; +} + +// From codersdk/workspaces.go +export interface WorkspaceFilter { + readonly q?: string; +} + +// From codersdk/workspaces.go +export interface WorkspaceHealth { + readonly healthy: boolean; + readonly failing_agents: readonly string[]; +} + +// From codersdk/workspaces.go +export interface WorkspaceOptions { + readonly include_deleted?: boolean; +} + +// From codersdk/workspaceproxy.go +export interface WorkspaceProxy extends Region { + readonly derp_enabled: boolean; + readonly derp_only: boolean; + readonly status?: WorkspaceProxyStatus; + readonly created_at: string; + readonly updated_at: string; + readonly deleted: boolean; + readonly version: string; +} + +// From codersdk/deployment.go +export interface WorkspaceProxyBuildInfo { + readonly workspace_proxy: boolean; + readonly dashboard_url: string; +} + +// From codersdk/workspaceproxy.go +export interface WorkspaceProxyStatus { + readonly status: ProxyHealthStatus; + readonly report?: ProxyHealthReport; + readonly checked_at: string; +} + +// From codersdk/workspaces.go +export interface WorkspaceQuota { + readonly credits_consumed: number; + readonly budget: number; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceResource { + readonly id: string; + readonly created_at: string; + readonly job_id: string; + readonly workspace_transition: WorkspaceTransition; + readonly type: string; + readonly name: string; + readonly hide: boolean; + readonly icon: string; + readonly agents?: readonly WorkspaceAgent[]; + readonly metadata?: readonly WorkspaceResourceMetadata[]; + readonly daily_cost: number; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceResourceMetadata { + readonly key: string; + readonly value: string; + readonly sensitive: boolean; +} + +// From codersdk/workspaces.go +export interface WorkspacesRequest extends Pagination { + readonly q?: string; +} + +// From codersdk/workspaces.go +export interface WorkspacesResponse { + readonly workspaces: readonly Workspace[]; + readonly count: number; +} + +// From codersdk/apikey.go +export type APIKeyScope = 'all' | 'application_connect'; +export const APIKeyScopes: APIKeyScope[] = ['all', 'application_connect']; + +// From codersdk/workspaceagents.go +export type AgentSubsystem = 'envbox' | 'envbuilder' | 'exectrace'; +export const AgentSubsystems: AgentSubsystem[] = [ + 'envbox', + 'envbuilder', + 'exectrace', +]; + +// From codersdk/audit.go +export type AuditAction = + | 'create' + | 'delete' + | 'login' + | 'logout' + | 'register' + | 'start' + | 'stop' + | 'write'; +export const AuditActions: AuditAction[] = [ + 'create', + 'delete', + 'login', + 'logout', + 'register', + 'start', + 'stop', + 'write', +]; + +// From codersdk/workspaces.go +export type AutomaticUpdates = 'always' | 'never'; +export const AutomaticUpdateses: AutomaticUpdates[] = ['always', 'never']; + +// From codersdk/workspacebuilds.go +export type BuildReason = 'autostart' | 'autostop' | 'initiator'; +export const BuildReasons: BuildReason[] = [ + 'autostart', + 'autostop', + 'initiator', +]; + +// From codersdk/workspaceagents.go +export type DisplayApp = + | 'port_forwarding_helper' + | 'ssh_helper' + | 'vscode' + | 'vscode_insiders' + | 'web_terminal'; +export const DisplayApps: DisplayApp[] = [ + 'port_forwarding_helper', + 'ssh_helper', + 'vscode', + 'vscode_insiders', + 'web_terminal', +]; + +// From codersdk/externalauth.go +export type EnhancedExternalAuthProvider = + | 'azure-devops' + | 'azure-devops-entra' + | 'bitbucket-cloud' + | 'bitbucket-server' + | 'gitea' + | 'github' + | 'gitlab' + | 'jfrog' + | 'slack'; +export const EnhancedExternalAuthProviders: EnhancedExternalAuthProvider[] = [ + 'azure-devops', + 'azure-devops-entra', + 'bitbucket-cloud', + 'bitbucket-server', + 'gitea', + 'github', + 'gitlab', + 'jfrog', + 'slack', +]; + +// From codersdk/deployment.go +export type Entitlement = 'entitled' | 'grace_period' | 'not_entitled'; +export const entitlements: Entitlement[] = [ + 'entitled', + 'grace_period', + 'not_entitled', +]; + +// From codersdk/deployment.go +export type Experiment = + | 'auto-fill-parameters' + | 'custom-roles' + | 'example' + | 'multi-organization'; +export const experiments: Experiment[] = [ + 'auto-fill-parameters', + 'custom-roles', + 'example', + 'multi-organization', +]; + +// From codersdk/deployment.go +export type FeatureName = + | 'access_control' + | 'advanced_template_scheduling' + | 'appearance' + | 'audit_log' + | 'browser_only' + | 'control_shared_ports' + | 'custom_roles' + | 'external_provisioner_daemons' + | 'external_token_encryption' + | 'high_availability' + | 'multiple_external_auth' + | 'scim' + | 'template_rbac' + | 'user_limit' + | 'user_role_management' + | 'workspace_batch_actions' + | 'workspace_proxy'; +export const FeatureNames: FeatureName[] = [ + 'access_control', + 'advanced_template_scheduling', + 'appearance', + 'audit_log', + 'browser_only', + 'control_shared_ports', + 'custom_roles', + 'external_provisioner_daemons', + 'external_token_encryption', + 'high_availability', + 'multiple_external_auth', + 'scim', + 'template_rbac', + 'user_limit', + 'user_role_management', + 'workspace_batch_actions', + 'workspace_proxy', +]; + +// From codersdk/groups.go +export type GroupSource = 'oidc' | 'user'; +export const GroupSources: GroupSource[] = ['oidc', 'user']; + +// From codersdk/insights.go +export type InsightsReportInterval = 'day' | 'week'; +export const InsightsReportIntervals: InsightsReportInterval[] = [ + 'day', + 'week', +]; + +// From codersdk/provisionerdaemons.go +export type JobErrorCode = 'REQUIRED_TEMPLATE_VARIABLES'; +export const JobErrorCodes: JobErrorCode[] = ['REQUIRED_TEMPLATE_VARIABLES']; + +// From codersdk/provisionerdaemons.go +export type LogLevel = 'debug' | 'error' | 'info' | 'trace' | 'warn'; +export const LogLevels: LogLevel[] = [ + 'debug', + 'error', + 'info', + 'trace', + 'warn', +]; + +// From codersdk/provisionerdaemons.go +export type LogSource = 'provisioner' | 'provisioner_daemon'; +export const LogSources: LogSource[] = ['provisioner', 'provisioner_daemon']; + +// From codersdk/apikey.go +export type LoginType = '' | 'github' | 'none' | 'oidc' | 'password' | 'token'; +export const LoginTypes: LoginType[] = [ + '', + 'github', + 'none', + 'oidc', + 'password', + 'token', +]; + +// From codersdk/oauth2.go +export type OAuth2ProviderGrantType = 'authorization_code' | 'refresh_token'; +export const OAuth2ProviderGrantTypes: OAuth2ProviderGrantType[] = [ + 'authorization_code', + 'refresh_token', +]; + +// From codersdk/oauth2.go +export type OAuth2ProviderResponseType = 'code'; +export const OAuth2ProviderResponseTypes: OAuth2ProviderResponseType[] = [ + 'code', +]; + +// From codersdk/deployment.go +export type PostgresAuth = 'awsiamrds' | 'password'; +export const PostgresAuths: PostgresAuth[] = ['awsiamrds', 'password']; + +// From codersdk/provisionerdaemons.go +export type ProvisionerJobStatus = + | 'canceled' + | 'canceling' + | 'failed' + | 'pending' + | 'running' + | 'succeeded' + | 'unknown'; +export const ProvisionerJobStatuses: ProvisionerJobStatus[] = [ + 'canceled', + 'canceling', + 'failed', + 'pending', + 'running', + 'succeeded', + 'unknown', +]; + +// From codersdk/workspaces.go +export type ProvisionerLogLevel = 'debug'; +export const ProvisionerLogLevels: ProvisionerLogLevel[] = ['debug']; + +// From codersdk/organizations.go +export type ProvisionerStorageMethod = 'file'; +export const ProvisionerStorageMethods: ProvisionerStorageMethod[] = ['file']; + +// From codersdk/organizations.go +export type ProvisionerType = 'echo' | 'terraform'; +export const ProvisionerTypes: ProvisionerType[] = ['echo', 'terraform']; + +// From codersdk/workspaceproxy.go +export type ProxyHealthStatus = + | 'ok' + | 'unhealthy' + | 'unreachable' + | 'unregistered'; +export const ProxyHealthStatuses: ProxyHealthStatus[] = [ + 'ok', + 'unhealthy', + 'unreachable', + 'unregistered', +]; + +// From codersdk/rbacresources_gen.go +export type RBACAction = + | 'application_connect' + | 'assign' + | 'create' + | 'delete' + | 'read' + | 'read_personal' + | 'ssh' + | 'start' + | 'stop' + | 'update' + | 'update_personal' + | 'use' + | 'view_insights'; +export const RBACActions: RBACAction[] = [ + 'application_connect', + 'assign', + 'create', + 'delete', + 'read', + 'read_personal', + 'ssh', + 'start', + 'stop', + 'update', + 'update_personal', + 'use', + 'view_insights', +]; + +// From codersdk/rbacresources_gen.go +export type RBACResource = + | '*' + | 'api_key' + | 'assign_org_role' + | 'assign_role' + | 'audit_log' + | 'debug_info' + | 'deployment_config' + | 'deployment_stats' + | 'file' + | 'group' + | 'license' + | 'oauth2_app' + | 'oauth2_app_code_token' + | 'oauth2_app_secret' + | 'organization' + | 'organization_member' + | 'provisioner_daemon' + | 'replicas' + | 'system' + | 'tailnet_coordinator' + | 'template' + | 'user' + | 'workspace' + | 'workspace_dormant' + | 'workspace_proxy'; +export const RBACResources: RBACResource[] = [ + '*', + 'api_key', + 'assign_org_role', + 'assign_role', + 'audit_log', + 'debug_info', + 'deployment_config', + 'deployment_stats', + 'file', + 'group', + 'license', + 'oauth2_app', + 'oauth2_app_code_token', + 'oauth2_app_secret', + 'organization', + 'organization_member', + 'provisioner_daemon', + 'replicas', + 'system', + 'tailnet_coordinator', + 'template', + 'user', + 'workspace', + 'workspace_dormant', + 'workspace_proxy', +]; + +// From codersdk/audit.go +export type ResourceType = + | 'api_key' + | 'convert_login' + | 'git_ssh_key' + | 'group' + | 'health_settings' + | 'license' + | 'oauth2_provider_app' + | 'oauth2_provider_app_secret' + | 'organization' + | 'template' + | 'template_version' + | 'user' + | 'workspace' + | 'workspace_build' + | 'workspace_proxy'; +export const ResourceTypes: ResourceType[] = [ + 'api_key', + 'convert_login', + 'git_ssh_key', + 'group', + 'health_settings', + 'license', + 'oauth2_provider_app', + 'oauth2_provider_app_secret', + 'organization', + 'template', + 'template_version', + 'user', + 'workspace', + 'workspace_build', + 'workspace_proxy', +]; + +// From codersdk/serversentevents.go +export type ServerSentEventType = 'data' | 'error' | 'ping'; +export const ServerSentEventTypes: ServerSentEventType[] = [ + 'data', + 'error', + 'ping', +]; + +// From codersdk/insights.go +export type TemplateAppsType = 'app' | 'builtin'; +export const TemplateAppsTypes: TemplateAppsType[] = ['app', 'builtin']; + +// From codersdk/insights.go +export type TemplateInsightsSection = 'interval_reports' | 'report'; +export const TemplateInsightsSections: TemplateInsightsSection[] = [ + 'interval_reports', + 'report', +]; + +// From codersdk/templates.go +export type TemplateRole = '' | 'admin' | 'use'; +export const TemplateRoles: TemplateRole[] = ['', 'admin', 'use']; + +// From codersdk/templateversions.go +export type TemplateVersionWarning = 'UNSUPPORTED_WORKSPACES'; +export const TemplateVersionWarnings: TemplateVersionWarning[] = [ + 'UNSUPPORTED_WORKSPACES', +]; + +// From codersdk/users.go +export type UserStatus = 'active' | 'dormant' | 'suspended'; +export const UserStatuses: UserStatus[] = ['active', 'dormant', 'suspended']; + +// From codersdk/templateversions.go +export type ValidationMonotonicOrder = 'decreasing' | 'increasing'; +export const ValidationMonotonicOrders: ValidationMonotonicOrder[] = [ + 'decreasing', + 'increasing', +]; + +// From codersdk/workspaceagents.go +export type WorkspaceAgentLifecycle = + | 'created' + | 'off' + | 'ready' + | 'shutdown_error' + | 'shutdown_timeout' + | 'shutting_down' + | 'start_error' + | 'start_timeout' + | 'starting'; +export const WorkspaceAgentLifecycles: WorkspaceAgentLifecycle[] = [ + 'created', + 'off', + 'ready', + 'shutdown_error', + 'shutdown_timeout', + 'shutting_down', + 'start_error', + 'start_timeout', + 'starting', +]; + +// From codersdk/workspaceagentportshare.go +export type WorkspaceAgentPortShareLevel = 'authenticated' | 'owner' | 'public'; +export const WorkspaceAgentPortShareLevels: WorkspaceAgentPortShareLevel[] = [ + 'authenticated', + 'owner', + 'public', +]; + +// From codersdk/workspaceagentportshare.go +export type WorkspaceAgentPortShareProtocol = 'http' | 'https'; +export const WorkspaceAgentPortShareProtocols: WorkspaceAgentPortShareProtocol[] = + ['http', 'https']; + +// From codersdk/workspaceagents.go +export type WorkspaceAgentStartupScriptBehavior = 'blocking' | 'non-blocking'; +export const WorkspaceAgentStartupScriptBehaviors: WorkspaceAgentStartupScriptBehavior[] = + ['blocking', 'non-blocking']; + +// From codersdk/workspaceagents.go +export type WorkspaceAgentStatus = + | 'connected' + | 'connecting' + | 'disconnected' + | 'timeout'; +export const WorkspaceAgentStatuses: WorkspaceAgentStatus[] = [ + 'connected', + 'connecting', + 'disconnected', + 'timeout', +]; + +// From codersdk/workspaceapps.go +export type WorkspaceAppHealth = + | 'disabled' + | 'healthy' + | 'initializing' + | 'unhealthy'; +export const WorkspaceAppHealths: WorkspaceAppHealth[] = [ + 'disabled', + 'healthy', + 'initializing', + 'unhealthy', +]; + +// From codersdk/workspaceapps.go +export type WorkspaceAppSharingLevel = 'authenticated' | 'owner' | 'public'; +export const WorkspaceAppSharingLevels: WorkspaceAppSharingLevel[] = [ + 'authenticated', + 'owner', + 'public', +]; + +// From codersdk/workspacebuilds.go +export type WorkspaceStatus = + | 'canceled' + | 'canceling' + | 'deleted' + | 'deleting' + | 'failed' + | 'pending' + | 'running' + | 'starting' + | 'stopped' + | 'stopping'; +export const WorkspaceStatuses: WorkspaceStatus[] = [ + 'canceled', + 'canceling', + 'deleted', + 'deleting', + 'failed', + 'pending', + 'running', + 'starting', + 'stopped', + 'stopping', +]; + +// From codersdk/workspacebuilds.go +export type WorkspaceTransition = 'delete' | 'start' | 'stop'; +export const WorkspaceTransitions: WorkspaceTransition[] = [ + 'delete', + 'start', + 'stop', +]; + +// From codersdk/workspaceproxy.go +export type RegionTypes = Region | WorkspaceProxy; + +// The code below is generated from codersdk/healthsdk. + +// From healthsdk/healthsdk.go +export interface AccessURLReport extends BaseReport { + readonly healthy: boolean; + readonly access_url: string; + readonly reachable: boolean; + readonly status_code: number; + readonly healthz_response: string; +} + +// From healthsdk/healthsdk.go +export interface BaseReport { + readonly error?: string; + readonly severity: HealthSeverity; + readonly warnings: readonly HealthMessage[]; + readonly dismissed: boolean; +} + +// From healthsdk/healthsdk.go +export interface DERPHealthReport extends BaseReport { + readonly healthy: boolean; + readonly regions: Record; + // Named type "tailscale.com/net/netcheck.Report" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly netcheck?: any; + readonly netcheck_err?: string; + readonly netcheck_logs: readonly string[]; +} + +// From healthsdk/healthsdk.go +export interface DERPNodeReport { + readonly healthy: boolean; + readonly severity: HealthSeverity; + readonly warnings: readonly HealthMessage[]; + readonly error?: string; + // Named type "tailscale.com/tailcfg.DERPNode" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly node?: any; + // Named type "tailscale.com/derp.ServerInfoMessage" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly node_info: any; + readonly can_exchange_messages: boolean; + readonly round_trip_ping: string; + readonly round_trip_ping_ms: number; + readonly uses_websocket: boolean; + readonly client_logs: readonly (readonly string[])[]; + readonly client_errs: readonly (readonly string[])[]; + readonly stun: STUNReport; +} + +// From healthsdk/healthsdk.go +export interface DERPRegionReport { + readonly healthy: boolean; + readonly severity: HealthSeverity; + readonly warnings: readonly HealthMessage[]; + readonly error?: string; + // Named type "tailscale.com/tailcfg.DERPRegion" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly region?: any; + readonly node_reports: readonly DERPNodeReport[]; +} + +// From healthsdk/healthsdk.go +export interface DatabaseReport extends BaseReport { + readonly healthy: boolean; + readonly reachable: boolean; + readonly latency: string; + readonly latency_ms: number; + readonly threshold_ms: number; +} + +// From healthsdk/healthsdk.go +export interface HealthSettings { + readonly dismissed_healthchecks: readonly HealthSection[]; +} + +// From healthsdk/healthsdk.go +export interface HealthcheckReport { + readonly time: string; + readonly healthy: boolean; + readonly severity: HealthSeverity; + readonly failing_sections: readonly HealthSection[]; + readonly derp: DERPHealthReport; + readonly access_url: AccessURLReport; + readonly websocket: WebsocketReport; + readonly database: DatabaseReport; + readonly workspace_proxy: WorkspaceProxyReport; + readonly provisioner_daemons: ProvisionerDaemonsReport; + readonly coder_version: string; +} + +// From healthsdk/healthsdk.go +export interface ProvisionerDaemonsReport extends BaseReport { + readonly items: readonly ProvisionerDaemonsReportItem[]; +} + +// From healthsdk/healthsdk.go +export interface ProvisionerDaemonsReportItem { + readonly provisioner_daemon: ProvisionerDaemon; + readonly warnings: readonly HealthMessage[]; +} + +// From healthsdk/healthsdk.go +export interface STUNReport { + readonly Enabled: boolean; + readonly CanSTUN: boolean; + readonly Error?: string; +} + +// From healthsdk/healthsdk.go +export interface UpdateHealthSettings { + readonly dismissed_healthchecks: readonly HealthSection[]; +} + +// From healthsdk/healthsdk.go +export interface WebsocketReport extends BaseReport { + readonly healthy: boolean; + readonly body: string; + readonly code: number; +} + +// From healthsdk/healthsdk.go +export interface WorkspaceProxyReport extends BaseReport { + readonly healthy: boolean; + readonly workspace_proxies: RegionsResponse; +} + +// From healthsdk/healthsdk.go +export type HealthSection = + | 'AccessURL' + | 'DERP' + | 'Database' + | 'ProvisionerDaemons' + | 'Websocket' + | 'WorkspaceProxy'; +export const HealthSections: HealthSection[] = [ + 'AccessURL', + 'DERP', + 'Database', + 'ProvisionerDaemons', + 'Websocket', + 'WorkspaceProxy', +]; + +// The code below is generated from coderd/healthcheck/health. + +// From health/model.go +export interface HealthMessage { + readonly code: HealthCode; + readonly message: string; +} + +// From health/model.go +export type HealthCode = + | 'EACS01' + | 'EACS02' + | 'EACS03' + | 'EACS04' + | 'EDB01' + | 'EDB02' + | 'EDERP01' + | 'EDERP02' + | 'EPD01' + | 'EPD02' + | 'EPD03' + | 'EUNKNOWN' + | 'EWP01' + | 'EWP02' + | 'EWP04' + | 'EWS01' + | 'EWS02' + | 'EWS03'; +export const HealthCodes: HealthCode[] = [ + 'EACS01', + 'EACS02', + 'EACS03', + 'EACS04', + 'EDB01', + 'EDB02', + 'EDERP01', + 'EDERP02', + 'EPD01', + 'EPD02', + 'EPD03', + 'EUNKNOWN', + 'EWP01', + 'EWP02', + 'EWP04', + 'EWS01', + 'EWS02', + 'EWS03', +]; + +// From health/model.go +export type HealthSeverity = 'error' | 'ok' | 'warning'; +export const HealthSeveritys: HealthSeverity[] = ['error', 'ok', 'warning']; + +// The code below is generated from github.com/coder/serpent. + +// From serpent/serpent.go +export type SerpentAnnotations = Record; + +// From serpent/serpent.go +export interface SerpentGroup { + readonly parent?: SerpentGroup; + readonly name?: string; + readonly yaml?: string; + readonly description?: string; +} + +// From serpent/option.go +export interface SerpentOption { + readonly name?: string; + readonly description?: string; + readonly required?: boolean; + readonly flag?: string; + readonly flag_shorthand?: string; + readonly env?: string; + readonly yaml?: string; + readonly default?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Golang interface, unable to resolve type. + readonly value?: any; + readonly annotations?: SerpentAnnotations; + readonly group?: SerpentGroup; + readonly use_instead?: readonly SerpentOption[]; + readonly hidden?: boolean; + readonly value_source?: SerpentValueSource; +} + +// From serpent/option.go +export type SerpentOptionSet = readonly SerpentOption[]; + +// From serpent/option.go +export type SerpentValueSource = '' | 'default' | 'env' | 'flag' | 'yaml'; +export const SerpentValueSources: SerpentValueSource[] = [ + '', + 'default', + 'env', + 'flag', + 'yaml', +]; diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts new file mode 100644 index 00000000..f8451116 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts @@ -0,0 +1,36 @@ +export type * from './api/typesGenerated'; +export type { + DeleteWorkspaceOptions, + GetLicensesResponse, + InsightsParams, + InsightsTemplateParams, +} from './api/api'; +import { Api } from './api/api'; + +// Union of all API properties that won't ever be relevant to Backstage users. +// Not a huge deal that they still exist at runtime; mainly concerned about +// whether they pollute Intellisense when someone is using the SDK. Most of +// these properties don't deal with APIs and are mainly helpers in Core +type PropertyToHide = + | 'getJFrogXRayScan' + | 'getCsrfToken' + | 'setSessionToken' + | 'setHost' + | 'getAvailableExperiments' + | 'login' + | 'logout' + | 'convertToOAUTH' + | 'waitForBuild' + | 'addMember' + | 'removeMember' + | 'getWorkspaceParameters'; + +// Wanted to have a CoderSdk class (mainly re-exporting the Api class as itself +// with the extra properties omitted). But because classes are wonky and exist +// as both runtime values and types, it didn't seem possible, even with things +// like class declarations. Making a new function is good enough for now, though +export type CoderSdk = Omit; +export function makeCoderSdk(): CoderSdk { + const api = new Api(); + return api as CoderSdk; +} diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/utils/delay.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/utils/delay.ts new file mode 100644 index 00000000..b915a7fb --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/utils/delay.ts @@ -0,0 +1,4 @@ +export const delay = (ms: number): Promise => + new Promise(res => { + setTimeout(res, ms); + }); diff --git a/yarn.lock b/yarn.lock index e7553d7d..c287f84a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8919,6 +8919,11 @@ dependencies: "@types/node" "*" +"@types/ua-parser-js@^0.7.39": + version "0.7.39" + resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz#832c58e460c9435e4e34bb866e85e9146e12cdbb" + integrity sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg== + "@types/unist@^2", "@types/unist@^2.0.0": version "2.0.10" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" @@ -11859,6 +11864,11 @@ dateformat@^3.0.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dayjs@^1.11.11: + version "1.11.11" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" + integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== + debounce@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" @@ -20248,14 +20258,6 @@ react-dom@^18.0.2: loose-envify "^1.1.0" scheduler "^0.23.0" -react-dom@^18.3.1: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" - react-double-scrollbar@0.0.15: version "0.0.15" resolved "https://registry.yarnpkg.com/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz#e915ab8cb3b959877075f49436debfdb04288fe4" @@ -21188,13 +21190,6 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" - schema-utils@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" @@ -22953,6 +22948,11 @@ typescript@~5.2.0: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== +ua-parser-js@^1.0.37: + version "1.0.37" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f" + integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" From c245950fba57a1432b1dcc2cf8386af8d1e71dd2 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 3 Jun 2024 10:23:17 -0400 Subject: [PATCH 3/9] chore(Coder plugin): update all Backstage code to use preview SDK (#131) * 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 * fix: add extra helpers to useCoderSdk * 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 --- .../src/api/CoderClient.test.ts | 60 +--- .../src/api/CoderClient.ts | 81 ++--- .../src/api/MockCoderSdk.ts | 48 --- .../src/api/UrlSync.test.ts | 8 +- .../backstage-plugin-coder/src/api/UrlSync.ts | 10 +- .../src/api/queryOptions.ts | 12 +- .../CoderErrorBoundary/CoderErrorBoundary.tsx | 2 +- .../CoderProvider/CoderAuthProvider.tsx | 24 +- .../CoderWorkspacesCard.test.tsx | 2 +- .../ReminderAccordion.test.tsx | 2 +- .../components/CoderWorkspacesCard/Root.tsx | 2 +- .../WorkspacesList.test.tsx | 4 +- .../CoderWorkspacesCard/WorkspacesList.tsx | 2 +- .../WorkspacesListItem.test.tsx | 10 +- .../WorkspacesListItem.tsx | 3 +- .../src/hooks/hookPolyfills.ts | 8 +- .../src/hooks/useCoderSdk.ts | 10 +- .../src/hooks/useCoderWorkspacesQuery.test.ts | 2 +- .../src/hooks/useCoderWorkspacesQuery.ts | 6 +- .../src/hooks/useUrlSync.test.tsx | 6 +- .../src/testHelpers/coderEntities.ts | 305 ++++++++++++++++++ .../src/testHelpers/mockBackstageData.ts | 18 +- ...CoderAppData.ts => mockCoderPluginData.ts} | 65 +++- .../src/testHelpers/server.ts | 19 +- .../src/typesConstants.ts | 85 ----- .../src/utils/workspaces.ts | 2 +- 26 files changed, 487 insertions(+), 309 deletions(-) delete mode 100644 plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts create mode 100644 plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts rename plugins/backstage-plugin-coder/src/testHelpers/{mockCoderAppData.ts => mockCoderPluginData.ts} (61%) diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts index 9addcd1a..2bfa6b24 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts @@ -1,8 +1,4 @@ -import { - CODER_AUTH_HEADER_KEY, - CoderClient, - disabledClientError, -} from './CoderClient'; +import { CODER_AUTH_HEADER_KEY, CoderClient } from './CoderClient'; import type { IdentityApi } from '@backstage/core-plugin-api'; import { UrlSync } from './UrlSync'; import { rest } from 'msw'; @@ -12,8 +8,8 @@ import { delay } from '../utils/time'; import { mockWorkspacesList, mockWorkspacesListForRepoSearch, -} from '../testHelpers/mockCoderAppData'; -import type { Workspace, WorkspacesResponse } from '../typesConstants'; +} from '../testHelpers/mockCoderPluginData'; +import type { Workspace, WorkspacesResponse } from './vendoredSdk'; import { getMockConfigApi, getMockDiscoveryApi, @@ -100,50 +96,6 @@ describe(`${CoderClient.name}`, () => { }); }); - describe('cleanupClient functionality', () => { - it('Will prevent any new SDK requests from going through', async () => { - const client = new CoderClient({ apis: getConstructorApis() }); - client.cleanupClient(); - - // Request should fail, even though token is valid - await expect(() => { - return client.syncToken(mockCoderAuthToken); - }).rejects.toThrow(disabledClientError); - - await expect(() => { - return client.sdk.getWorkspaces({ - q: 'owner:me', - limit: 0, - }); - }).rejects.toThrow(disabledClientError); - }); - - it('Will abort any pending requests', async () => { - const client = new CoderClient({ - initialToken: mockCoderAuthToken, - apis: getConstructorApis(), - }); - - // Sanity check to ensure that request can still go through normally - const workspacesPromise1 = client.sdk.getWorkspaces({ - q: 'owner:me', - limit: 0, - }); - - await expect(workspacesPromise1).resolves.toEqual({ - workspaces: mockWorkspacesList, - count: mockWorkspacesList.length, - }); - - const workspacesPromise2 = client.sdk.getWorkspaces({ - q: 'owner:me', - limit: 0, - }); - client.cleanupClient(); - await expect(() => workspacesPromise2).rejects.toThrow(); - }); - }); - // Eventually the Coder SDK is going to get too big to test every single // function. Focus tests on the functionality specifically being patched in // for Backstage @@ -180,10 +132,10 @@ describe(`${CoderClient.name}`, () => { }); const { urlSync } = apis; - const apiEndpoint = await urlSync.getApiEndpoint(); + const assetsEndpoint = await urlSync.getAssetsEndpoint(); - const allWorkspacesAreRemapped = !workspaces.some(ws => - ws.template_icon.startsWith(apiEndpoint), + const allWorkspacesAreRemapped = workspaces.every(ws => + ws.template_icon.startsWith(assetsEndpoint), ); expect(allWorkspacesAreRemapped).toBe(true); diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.ts index 7c09f72c..4c5333dd 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.ts @@ -1,19 +1,19 @@ -import globalAxios, { +import { AxiosError, - type AxiosInstance, type InternalAxiosRequestConfig as RequestConfig, } from 'axios'; import { type IdentityApi, createApiRef } from '@backstage/core-plugin-api'; -import { - type Workspace, - CODER_API_REF_ID_PREFIX, - WorkspacesRequest, - WorkspacesResponse, - User, -} from '../typesConstants'; +import { CODER_API_REF_ID_PREFIX } from '../typesConstants'; import type { UrlSync } from './UrlSync'; import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; -import { CoderSdk } from './MockCoderSdk'; +import { + type CoderSdk, + type User, + type Workspace, + type WorkspacesRequest, + type WorkspacesResponse, + makeCoderSdk, +} from './vendoredSdk'; export const CODER_AUTH_HEADER_KEY = 'Coder-Session-Token'; const DEFAULT_REQUEST_TIMEOUT_MS = 20_000; @@ -39,11 +39,6 @@ type CoderClientApi = Readonly<{ * Return value indicates whether the token is valid. */ syncToken: (newToken: string) => Promise; - - /** - * Cleans up a client instance, removing its links to all external systems. - */ - cleanupClient: () => void; }>; const sharedCleanupAbortReason = new DOMException( @@ -59,19 +54,30 @@ export const disabledClientError = new Error( ); type ConstructorInputs = Readonly<{ + /** + * initialToken is strictly for testing, and is basically limited to making it + * easier to test API logic. + * + * If trying to test UI logic that depends on CoderClient, it's probably + * better to interact with CoderClient indirectly through the auth components, + * so that React state is aware of everything. + */ initialToken?: string; - requestTimeoutMs?: number; + requestTimeoutMs?: number; apis: Readonly<{ urlSync: UrlSync; identityApi: IdentityApi; }>; }>; +type RequestInterceptor = ( + config: RequestConfig, +) => RequestConfig | Promise; + export class CoderClient implements CoderClientApi { private readonly urlSync: UrlSync; private readonly identityApi: IdentityApi; - private readonly axios: AxiosInstance; private readonly requestTimeoutMs: number; private readonly cleanupController: AbortController; @@ -82,33 +88,28 @@ export class CoderClient implements CoderClientApi { constructor(inputs: ConstructorInputs) { const { - apis, initialToken, + apis: { urlSync, identityApi }, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, } = inputs; - const { urlSync, identityApi } = apis; this.urlSync = urlSync; this.identityApi = identityApi; - this.axios = globalAxios.create(); - this.loadedSessionToken = initialToken; this.requestTimeoutMs = requestTimeoutMs; - this.cleanupController = new AbortController(); this.trackedEjectionIds = new Set(); - this.sdk = this.getBackstageCoderSdk(this.axios); + this.sdk = this.createBackstageCoderSdk(); this.addBaseRequestInterceptors(); } private addRequestInterceptor( - requestInterceptor: ( - config: RequestConfig, - ) => RequestConfig | Promise, + requestInterceptor: RequestInterceptor, errorInterceptor?: (error: unknown) => unknown, ): number { - const ejectionId = this.axios.interceptors.request.use( + const axios = this.sdk.getAxiosInstance(); + const ejectionId = axios.interceptors.request.use( requestInterceptor, errorInterceptor, ); @@ -120,7 +121,8 @@ export class CoderClient implements CoderClientApi { private removeRequestInterceptorById(ejectionId: number): boolean { // Even if we somehow pass in an ID that hasn't been associated with the // Axios instance, that's a noop. No harm in calling method no matter what - this.axios.interceptors.request.eject(ejectionId); + const axios = this.sdk.getAxiosInstance(); + axios.interceptors.request.eject(ejectionId); if (!this.trackedEjectionIds.has(ejectionId)) { return false; @@ -179,10 +181,8 @@ export class CoderClient implements CoderClientApi { this.addRequestInterceptor(baseRequestInterceptor, baseErrorInterceptor); } - private getBackstageCoderSdk( - axiosInstance: AxiosInstance, - ): BackstageCoderSdk { - const baseSdk = new CoderSdk(axiosInstance); + private createBackstageCoderSdk(): BackstageCoderSdk { + const baseSdk = makeCoderSdk(); const getWorkspaces: (typeof baseSdk)['getWorkspaces'] = async request => { const workspacesRes = await baseSdk.getWorkspaces(request); @@ -335,23 +335,6 @@ export class CoderClient implements CoderClientApi { this.removeRequestInterceptorById(validationId); } }; - - cleanupClient = (): void => { - this.trackedEjectionIds.forEach(id => { - this.axios.interceptors.request.eject(id); - }); - - this.trackedEjectionIds.clear(); - this.cleanupController.abort(sharedCleanupAbortReason); - this.loadedSessionToken = undefined; - - // Not using this.addRequestInterceptor, because we don't want to track this - // interceptor at all. It should never be ejected once the client has been - // disabled - this.axios.interceptors.request.use(() => { - throw disabledClientError; - }); - }; } function appendParamToQuery( diff --git a/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts b/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts deleted file mode 100644 index 3100242b..00000000 --- a/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @file This is a temporary (and significantly limited) implementation of the - * "Coder SDK" that will eventually be imported from Coder core - * - * @todo Replace this with a full, proper implementation, and then expose it to - * plugin users. - */ -import globalAxios, { type AxiosInstance } from 'axios'; -import { - type User, - type WorkspacesRequest, - type WorkspacesResponse, -} from '../typesConstants'; - -type CoderSdkApi = { - getAuthenticatedUser: () => Promise; - getWorkspaces: (request: WorkspacesRequest) => Promise; -}; - -export class CoderSdk implements CoderSdkApi { - private readonly axios: AxiosInstance; - - constructor(axiosInstance?: AxiosInstance) { - this.axios = axiosInstance ?? globalAxios.create(); - } - - getWorkspaces = async ( - request: WorkspacesRequest, - ): Promise => { - const urlParams = new URLSearchParams({ - q: request.q ?? '', - limit: String(request.limit || 0), - after_id: request.after_id ?? '', - offset: String(request.offset || 0), - }); - - const response = await this.axios.get( - `/workspaces?${urlParams.toString()}`, - ); - - return response.data; - }; - - getAuthenticatedUser = async (): Promise => { - const response = await this.axios.get('/users/me'); - return response.data; - }; -} diff --git a/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts b/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts index 4932edea..62001e4e 100644 --- a/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts +++ b/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts @@ -4,8 +4,8 @@ import { getMockConfigApi, getMockDiscoveryApi, mockBackstageAssetsEndpoint, - mockBackstageApiEndpoint, mockBackstageUrlRoot, + mockBackstageApiEndpointWithoutSdkPath, } from '../testHelpers/mockBackstageData'; // Tests have to assume that DiscoveryApi and ConfigApi will always be in sync, @@ -23,7 +23,7 @@ describe(`${UrlSync.name}`, () => { const cachedUrls = urlSync.getCachedUrls(); expect(cachedUrls).toEqual({ baseUrl: mockBackstageUrlRoot, - apiRoute: mockBackstageApiEndpoint, + apiRoute: mockBackstageApiEndpointWithoutSdkPath, assetsRoute: mockBackstageAssetsEndpoint, }); }); @@ -50,7 +50,7 @@ describe(`${UrlSync.name}`, () => { expect(newSnapshot).toEqual({ baseUrl: 'blah', - apiRoute: 'blah/coder/api/v2', + apiRoute: 'blah/coder', assetsRoute: 'blah/coder', }); }); @@ -76,7 +76,7 @@ describe(`${UrlSync.name}`, () => { expect(onChange).toHaveBeenCalledWith({ baseUrl: 'blah', - apiRoute: 'blah/coder/api/v2', + apiRoute: 'blah/coder', assetsRoute: 'blah/coder', } satisfies UrlSyncSnapshot); diff --git a/plugins/backstage-plugin-coder/src/api/UrlSync.ts b/plugins/backstage-plugin-coder/src/api/UrlSync.ts index ae05294b..8b3548d6 100644 --- a/plugins/backstage-plugin-coder/src/api/UrlSync.ts +++ b/plugins/backstage-plugin-coder/src/api/UrlSync.ts @@ -42,14 +42,10 @@ const PROXY_URL_KEY_FOR_DISCOVERY_API = 'proxy'; type UrlPrefixes = Readonly<{ proxyPrefix: string; - apiRoutePrefix: string; - assetsRoutePrefix: string; }>; export const defaultUrlPrefixes = { proxyPrefix: `/api/proxy`, - apiRoutePrefix: '/api/v2', - assetsRoutePrefix: '', // Deliberately left as empty string } as const satisfies UrlPrefixes; export type UrlSyncSnapshot = Readonly<{ @@ -104,12 +100,10 @@ export class UrlSync implements UrlSyncApi { } private prepareNewSnapshot(newProxyUrl: string): UrlSyncSnapshot { - const { assetsRoutePrefix, apiRoutePrefix } = this.urlPrefixes; - return { baseUrl: newProxyUrl.replace(proxyRouteReplacer, ''), - assetsRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}${assetsRoutePrefix}`, - apiRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}${apiRoutePrefix}`, + assetsRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}`, + apiRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}`, }; } diff --git a/plugins/backstage-plugin-coder/src/api/queryOptions.ts b/plugins/backstage-plugin-coder/src/api/queryOptions.ts index b10ecfe2..4e55861d 100644 --- a/plugins/backstage-plugin-coder/src/api/queryOptions.ts +++ b/plugins/backstage-plugin-coder/src/api/queryOptions.ts @@ -1,5 +1,5 @@ import type { UseQueryOptions } from '@tanstack/react-query'; -import type { Workspace, WorkspacesRequest } from '../typesConstants'; +import type { Workspace, WorkspacesRequest } from './vendoredSdk'; import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; import type { BackstageCoderSdk } from './CoderClient'; import type { CoderAuth } from '../components/CoderProvider'; @@ -44,13 +44,13 @@ function getSharedWorkspacesQueryKey(coderQuery: string) { type WorkspacesFetchInputs = Readonly<{ auth: CoderAuth; - coderSdk: BackstageCoderSdk; + sdk: BackstageCoderSdk; coderQuery: string; }>; export function workspaces({ auth, - coderSdk, + sdk, coderQuery, }: WorkspacesFetchInputs): UseQueryOptions { const enabled = auth.isAuthenticated; @@ -61,7 +61,7 @@ export function workspaces({ keepPreviousData: enabled && coderQuery !== '', refetchInterval: getCoderWorkspacesRefetchInterval, queryFn: async () => { - const res = await coderSdk.getWorkspaces({ + const res = await sdk.getWorkspaces({ q: coderQuery, limit: 0, }); @@ -79,7 +79,7 @@ type WorkspacesByRepoFetchInputs = Readonly< export function workspacesByRepo({ coderQuery, - coderSdk, + sdk, auth, workspacesConfig, }: WorkspacesByRepoFetchInputs): UseQueryOptions { @@ -95,7 +95,7 @@ export function workspacesByRepo({ refetchInterval: getCoderWorkspacesRefetchInterval, queryFn: async () => { const request: WorkspacesRequest = { q: coderQuery, limit: 0 }; - const res = await coderSdk.getWorkspacesByRepo(request, workspacesConfig); + const res = await sdk.getWorkspacesByRepo(request, workspacesConfig); return res.workspaces; }, }; diff --git a/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx b/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx index c1f2bc61..5843a180 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx @@ -39,7 +39,7 @@ class ErrorBoundaryCore extends Component< render() { const { children, fallbackUi } = this.props; - return this.state.hasError ? fallbackUi : children; + return <>{this.state.hasError ? fallbackUi : children}; } } diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index c9b6fbb1..664bb311 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -165,19 +165,23 @@ function useAuthState(): CoderAuth { return unsubscribe; }, [queryClient]); + const registerNewToken = useCallback((newToken: string) => { + if (newToken !== '') { + setAuthToken(newToken); + } + }, []); + + const ejectToken = useCallback(() => { + setAuthToken(''); + window.localStorage.removeItem(TOKEN_STORAGE_KEY); + queryClient.removeQueries({ queryKey: [CODER_QUERY_KEY_PREFIX] }); + }, [queryClient]); + return { ...authState, isAuthenticated: validAuthStatuses.includes(authState.status), - registerNewToken: newToken => { - if (newToken !== '') { - setAuthToken(newToken); - } - }, - ejectToken: () => { - setAuthToken(''); - window.localStorage.removeItem(TOKEN_STORAGE_KEY); - queryClient.removeQueries({ queryKey: [CODER_QUERY_KEY_PREFIX] }); - }, + registerNewToken, + ejectToken, }; } diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx index a8cbef6c..8acc04a1 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx @@ -10,7 +10,7 @@ import { mockWorkspaceNoParameters, mockWorkspaceWithMatch2, mockWorkspacesList, -} from '../../testHelpers/mockCoderAppData'; +} from '../../testHelpers/mockCoderPluginData'; import { type CoderAuthStatus } from '../CoderProvider'; import { CoderWorkspacesCard } from './CoderWorkspacesCard'; import userEvent from '@testing-library/user-event'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx index 0ae1d918..5be7284b 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderInCoderEnvironment } from '../../testHelpers/setup'; -import type { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData'; import { type WorkspacesCardContext, diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx index 0866d95a..452f0a9c 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx @@ -15,7 +15,7 @@ import { useCoderWorkspacesConfig, type CoderWorkspacesConfig, } from '../../hooks/useCoderWorkspacesConfig'; -import type { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { useCoderWorkspacesQuery } from '../../hooks/useCoderWorkspacesQuery'; import { CoderAuthFormCardWrapper } from '../CoderAuthFormCardWrapper'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx index 50bc1de1..bc7e0273 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx @@ -3,8 +3,8 @@ import { type WorkspacesListProps, WorkspacesList } from './WorkspacesList'; import { renderInCoderEnvironment } from '../../testHelpers/setup'; import { CardContext, WorkspacesCardContext, WorkspacesQuery } from './Root'; import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData'; -import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData'; -import { Workspace } from '../../typesConstants'; +import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderPluginData'; +import type { Workspace } from '../../api/vendoredSdk'; import { screen } from '@testing-library/react'; type RenderInputs = Readonly<{ diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx index 1e47b08a..9301d6a4 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx @@ -1,7 +1,7 @@ import React, { type HTMLAttributes, type ReactNode, Fragment } from 'react'; import { type Theme, makeStyles } from '@material-ui/core'; -import type { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { useWorkspacesCardContext } from './Root'; import { WorkspacesListItem } from './WorkspacesListItem'; import { Placeholder } from './Placeholder'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx index 03ff2623..471d3356 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx @@ -1,9 +1,13 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { renderInCoderEnvironment } from '../../testHelpers/setup'; -import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData'; -import type { Workspace } from '../../typesConstants'; +import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderPluginData'; +import type { Workspace } from '../../api/vendoredSdk'; import { WorkspacesListItem } from './WorkspacesListItem'; +import { + MockWorkspaceAgent, + MockWorkspaceResource, +} from '../../testHelpers/coderEntities'; type RenderInput = Readonly<{ isOnline?: boolean; @@ -19,9 +23,11 @@ async function renderListItem(inputs?: RenderInput) { status: isOnline ? 'running' : 'stopped', resources: [ { + ...MockWorkspaceResource, id: '1', agents: [ { + ...MockWorkspaceAgent, id: '2', status: isOnline ? 'connected' : 'disconnected', }, diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx index f7292e51..a5a588ae 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx @@ -11,7 +11,8 @@ import { useId } from '../../hooks/hookPolyfills'; import { useCoderAppConfig } from '../CoderProvider'; import { getWorkspaceAgentStatuses } from '../../utils/workspaces'; -import type { Workspace, WorkspaceStatus } from '../../typesConstants'; +import type { WorkspaceStatus } from '../../api/vendoredSdk'; +import type { Workspace } from '../../api/vendoredSdk'; import { WorkspacesListIcon } from './WorkspacesListIcon'; import { VisuallyHidden } from '../VisuallyHidden'; diff --git a/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts b/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts index 3b777c5e..ce15f948 100644 --- a/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts +++ b/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts @@ -25,5 +25,11 @@ function useIdPolyfill(): string { return readonlyId; } +const ReactWithNewerHooks = React as typeof React & { + useId?: () => string; +}; + export const useId = - typeof React.useId === 'undefined' ? useIdPolyfill : React.useId; + typeof ReactWithNewerHooks.useId === 'undefined' + ? useIdPolyfill + : ReactWithNewerHooks.useId; diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts index 8fbec12c..7b7017a1 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts @@ -1,7 +1,13 @@ +/** + * @file This defines the general helper for accessing the Coder SDK from + * Backstage in a type-safe way. + * + * This hook is meant to be used both internally AND externally. + */ import { useApi } from '@backstage/core-plugin-api'; import { coderClientApiRef, type BackstageCoderSdk } from '../api/CoderClient'; export function useCoderSdk(): BackstageCoderSdk { - const coderClient = useApi(coderClientApiRef); - return coderClient.sdk; + const { sdk } = useApi(coderClientApiRef); + return sdk; } diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts index d29e64a5..49535619 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts @@ -6,7 +6,7 @@ import { mockCoderWorkspacesConfig } from '../testHelpers/mockBackstageData'; import { mockWorkspaceNoParameters, mockWorkspacesList, -} from '../testHelpers/mockCoderAppData'; +} from '../testHelpers/mockCoderPluginData'; beforeAll(() => { jest.useFakeTimers(); diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts index 4e41ef86..63b4f2f7 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts @@ -13,13 +13,13 @@ export function useCoderWorkspacesQuery({ coderQuery, workspacesConfig, }: QueryInput) { + const sdk = useCoderSdk(); const auth = useInternalCoderAuth(); - const coderSdk = useCoderSdk(); const hasRepoData = workspacesConfig && workspacesConfig.repoUrl; const queryOptions = hasRepoData - ? workspacesByRepo({ auth, coderSdk, coderQuery, workspacesConfig }) - : workspaces({ auth, coderSdk, coderQuery }); + ? workspacesByRepo({ auth, sdk, coderQuery, workspacesConfig }) + : workspaces({ auth, sdk, coderQuery }); return useQuery(queryOptions); } diff --git a/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx b/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx index 164242f7..90cac33d 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx +++ b/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx @@ -6,13 +6,13 @@ import { type UseUrlSyncResult, useUrlSync } from './useUrlSync'; import type { DiscoveryApi } from '@backstage/core-plugin-api'; import { mockBackstageAssetsEndpoint, - mockBackstageApiEndpoint, mockBackstageUrlRoot, getMockConfigApi, + mockBackstageApiEndpointWithoutSdkPath, } from '../testHelpers/mockBackstageData'; function renderUseUrlSync() { - let proxyEndpoint: string = mockBackstageApiEndpoint; + let proxyEndpoint: string = mockBackstageApiEndpointWithoutSdkPath; const mockDiscoveryApi: DiscoveryApi = { getBaseUrl: async () => proxyEndpoint, }; @@ -53,7 +53,7 @@ describe(`${useUrlSync.name}`, () => { state: { baseUrl: mockBackstageUrlRoot, assetsRoute: mockBackstageAssetsEndpoint, - apiRoute: mockBackstageApiEndpoint, + apiRoute: mockBackstageApiEndpointWithoutSdkPath, }, }), ); diff --git a/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts b/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts new file mode 100644 index 00000000..b5cf5abf --- /dev/null +++ b/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts @@ -0,0 +1,305 @@ +/** + * @file This is a subset of the mock data from the Coder OSS repo. No values + * are modified; if any values should be patched for Backstage testing, those + * should be updated in the mockCoderPluginData.ts file. + * + * @see {@link https://github.com/coder/coder/blob/main/site/src/testHelpers/entities.ts} + */ +import type * as TypesGen from '../api/vendoredSdk'; + +const MockOrganization: TypesGen.Organization = { + id: 'fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0', + name: 'Test Organization', + created_at: '', + updated_at: '', + is_default: true, +}; + +const MockOwnerRole: TypesGen.Role = { + name: 'owner', + display_name: 'Owner', + site_permissions: [], + organization_permissions: {}, + user_permissions: [], + organization_id: '', +}; + +export const MockUser: TypesGen.User = { + id: 'test-user', + username: 'TestUser', + email: 'test@coder.com', + created_at: '', + status: 'active', + organization_ids: [MockOrganization.id], + roles: [MockOwnerRole], + avatar_url: 'https://avatars.githubusercontent.com/u/95932066?s=200&v=4', + last_seen_at: '', + login_type: 'password', + theme_preference: '', + name: '', +}; + +const MockProvisionerJob: TypesGen.ProvisionerJob = { + created_at: '', + id: 'test-provisioner-job', + status: 'succeeded', + file_id: MockOrganization.id, + completed_at: '2022-05-17T17:39:01.382927298Z', + tags: { + scope: 'organization', + owner: '', + wowzers: 'whatatag', + isCapable: 'false', + department: 'engineering', + dreaming: 'true', + }, + queue_position: 0, + queue_size: 0, +}; + +const MockProvisioner: TypesGen.ProvisionerDaemon = { + created_at: '2022-05-17T17:39:01.382927298Z', + id: 'test-provisioner', + name: 'Test Provisioner', + provisioners: ['echo'], + tags: { scope: 'organization' }, + version: 'v2.34.5', + api_version: '1.0', +}; + +const MockTemplateVersion: TypesGen.TemplateVersion = { + id: 'test-template-version', + created_at: '2022-05-17T17:39:01.382927298Z', + updated_at: '2022-05-17T17:39:01.382927298Z', + template_id: 'test-template', + job: MockProvisionerJob, + name: 'test-version', + message: 'first version', + readme: `--- +name:Template test +--- +## Instructions +You can add instructions here + +[Some link info](https://coder.com)`, + created_by: MockUser, + archived: false, +}; + +const MockWorkspaceAgentLogSource: TypesGen.WorkspaceAgentLogSource = { + created_at: '2023-05-04T11:30:41.402072Z', + id: 'dc790496-eaec-4f88-a53f-8ce1f61a1fff', + display_name: 'Startup Script', + icon: '', + workspace_agent_id: '', +}; + +const MockBuildInfo: TypesGen.BuildInfoResponse = { + agent_api_version: '1.0', + external_url: 'file:///mock-url', + version: 'v99.999.9999+c9cdf14', + dashboard_url: 'https:///mock-url', + workspace_proxy: false, + upgrade_message: 'My custom upgrade message', + deployment_id: '510d407f-e521-4180-b559-eab4a6d802b8', +}; + +const MockWorkspaceApp: TypesGen.WorkspaceApp = { + id: 'test-app', + slug: 'test-app', + display_name: 'Test App', + icon: '', + subdomain: false, + health: 'disabled', + external: false, + url: '', + sharing_level: 'owner', + healthcheck: { + url: '', + interval: 0, + threshold: 0, + }, +}; + +const MockWorkspaceAgentScript: TypesGen.WorkspaceAgentScript = { + log_source_id: MockWorkspaceAgentLogSource.id, + cron: '', + log_path: '', + run_on_start: true, + run_on_stop: false, + script: "echo 'hello world'", + start_blocks_login: false, + timeout: 0, +}; + +export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { + apps: [MockWorkspaceApp], + architecture: 'amd64', + created_at: '', + environment_variables: {}, + id: 'test-workspace-agent', + name: 'a-workspace-agent', + operating_system: 'linux', + resource_id: '', + status: 'connected', + updated_at: '', + version: MockBuildInfo.version, + api_version: '1.0', + latency: { + 'Coder Embedded DERP': { + latency_ms: 32.55, + preferred: true, + }, + }, + connection_timeout_seconds: 120, + troubleshooting_url: 'https://coder.com/troubleshoot', + lifecycle_state: 'starting', + logs_length: 0, + logs_overflowed: false, + log_sources: [MockWorkspaceAgentLogSource], + scripts: [MockWorkspaceAgentScript], + startup_script_behavior: 'non-blocking', + subsystems: ['envbox', 'exectrace'], + health: { + healthy: true, + }, + display_apps: [ + 'ssh_helper', + 'port_forwarding_helper', + 'vscode', + 'vscode_insiders', + 'web_terminal', + ], +}; + +export const MockWorkspaceResource: TypesGen.WorkspaceResource = { + id: 'test-workspace-resource', + name: 'a-workspace-resource', + agents: [MockWorkspaceAgent], + created_at: '', + job_id: '', + type: 'google_compute_disk', + workspace_transition: 'start', + hide: false, + icon: '', + metadata: [{ key: 'size', value: '32GB', sensitive: false }], + daily_cost: 10, +}; + +const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { + build_number: 1, + created_at: '2022-05-17T17:39:01.382927298Z', + id: '1', + initiator_id: MockUser.id, + initiator_name: MockUser.username, + job: MockProvisionerJob, + template_version_id: MockTemplateVersion.id, + template_version_name: MockTemplateVersion.name, + transition: 'start', + updated_at: '2022-05-17T17:39:01.382927298Z', + workspace_name: 'test-workspace', + workspace_owner_id: MockUser.id, + workspace_owner_name: MockUser.username, + workspace_owner_avatar_url: MockUser.avatar_url, + workspace_id: '759f1d46-3174-453d-aa60-980a9c1442f3', + deadline: '2022-05-17T23:39:00.00Z', + reason: 'initiator', + resources: [MockWorkspaceResource], + status: 'running', + daily_cost: 20, +}; + +const MockTemplate: TypesGen.Template = { + id: 'test-template', + created_at: '2022-05-17T17:39:01.382927298Z', + updated_at: '2022-05-18T17:39:01.382927298Z', + organization_id: MockOrganization.id, + name: 'test-template', + display_name: 'Test Template', + provisioner: MockProvisioner.provisioners[0], + active_version_id: MockTemplateVersion.id, + active_user_count: 1, + build_time_stats: { + start: { + P50: 1000, + P95: 1500, + }, + stop: { + P50: 1000, + P95: 1500, + }, + delete: { + P50: 1000, + P95: 1500, + }, + }, + description: 'This is a test description.', + default_ttl_ms: 24 * 60 * 60 * 1000, + activity_bump_ms: 1 * 60 * 60 * 1000, + autostop_requirement: { + days_of_week: ['sunday'], + weeks: 1, + }, + autostart_requirement: { + days_of_week: [ + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday', + ], + }, + created_by_id: 'test-creator-id', + created_by_name: 'test_creator', + icon: '/icon/code.svg', + allow_user_cancel_workspace_jobs: true, + failure_ttl_ms: 0, + time_til_dormant_ms: 0, + time_til_dormant_autodelete_ms: 0, + allow_user_autostart: true, + allow_user_autostop: true, + require_active_version: false, + deprecated: false, + deprecation_message: '', + max_port_share_level: 'public', +}; + +const MockWorkspaceAutostartEnabled: TypesGen.UpdateWorkspaceAutostartRequest = + { + // Runs at 9:30am Monday through Friday using Canada/Eastern + // (America/Toronto) time + schedule: 'CRON_TZ=Canada/Eastern 30 9 * * 1-5', + }; + +export const MockWorkspace: TypesGen.Workspace = { + id: 'test-workspace', + name: 'Test-Workspace', + created_at: '', + updated_at: '', + template_id: MockTemplate.id, + template_name: MockTemplate.name, + template_icon: MockTemplate.icon, + template_display_name: MockTemplate.display_name, + template_allow_user_cancel_workspace_jobs: + MockTemplate.allow_user_cancel_workspace_jobs, + template_active_version_id: MockTemplate.active_version_id, + template_require_active_version: MockTemplate.require_active_version, + outdated: false, + owner_id: MockUser.id, + organization_id: MockOrganization.id, + owner_name: MockUser.username, + owner_avatar_url: 'https://avatars.githubusercontent.com/u/7122116?v=4', + autostart_schedule: MockWorkspaceAutostartEnabled.schedule, + ttl_ms: 2 * 60 * 60 * 1000, + latest_build: MockWorkspaceBuild, + last_used_at: '2022-05-16T15:29:10.302441433Z', + health: { + healthy: true, + failing_agents: [], + }, + automatic_updates: 'never', + allow_renames: true, + favorite: false, +}; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts index 34f11218..8c96f8d2 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts @@ -67,13 +67,25 @@ export const rawRepoUrl = `${cleanedRepoUrl}/tree/main/`; export const mockBackstageUrlRoot = 'http://localhost:7007'; /** - * The API endpoint to use with the mock server during testing. + * A version of the mock API endpoint that doesn't have the Coder API versioning + * prefix. Mainly used for tests that need to assert that the core API URL is + * formatted correctly, before the CoderSdk adds anything else to the end + * + * The string literal expression is complicated, but hover over it to see what + * the final result is. + */ +export const mockBackstageApiEndpointWithoutSdkPath = + `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}` as const; + +/** + * The API endpoint to use with the mock server during testing. Adds additional + * path information that will normally be added via the Coder SDK. * * The string literal expression is complicated, but hover over it to see what * the final result is. */ export const mockBackstageApiEndpoint = - `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}${defaultUrlPrefixes.apiRoutePrefix}` as const; + `${mockBackstageApiEndpointWithoutSdkPath}/api/v2` as const; /** * The assets endpoint to use during testing. @@ -82,7 +94,7 @@ export const mockBackstageApiEndpoint = * the final result is. */ export const mockBackstageAssetsEndpoint = - `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}${defaultUrlPrefixes.assetsRoutePrefix}` as const; + `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}` as const; export const mockBearerToken = 'This-is-an-opaque-value-by-design'; export const mockCoderAuthToken = 'ZG0HRy2gGN-mXljc1s5FqtE8WUJ4sUc5X'; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts similarity index 61% rename from plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts rename to plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts index 412e0e05..a3bfb10d 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts @@ -1,21 +1,45 @@ -import type { Workspace } from '../typesConstants'; -import { mockBackstageApiEndpoint } from './mockBackstageData'; +import type { User, Workspace } from '../api/vendoredSdk'; +import { + MockUser, + MockWorkspace, + MockWorkspaceAgent, + MockWorkspaceResource, +} from './coderEntities'; +import { + mockBackstageApiEndpoint, + mockBackstageAssetsEndpoint, +} from './mockBackstageData'; + +export const mockUserWithProxyUrls: User = { + ...MockUser, + avatar_url: `${mockBackstageAssetsEndpoint}/blueberry.png`, +}; /** * The main mock for a workspace whose repo URL matches cleanedRepoUrl */ export const mockWorkspaceWithMatch: Workspace = { + ...MockWorkspace, id: 'workspace-with-match', name: 'Test-Workspace', template_icon: `${mockBackstageApiEndpoint}/emojis/dog.svg`, owner_name: 'lil brudder', + latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-with-match-build', status: 'running', resources: [ { + ...MockWorkspaceResource, id: 'workspace-with-match-resource', - agents: [{ id: 'test-workspace-agent', status: 'connected' }], + agents: [ + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent', + status: 'connected', + }, + ], }, ], }, @@ -28,17 +52,27 @@ export const mockWorkspaceWithMatch: Workspace = { * return multiple values back */ export const mockWorkspaceWithMatch2: Workspace = { + ...MockWorkspace, id: 'workspace-with-match-2', name: 'Another-Test', template_icon: `${mockBackstageApiEndpoint}/emojis/z.svg`, owner_name: 'Coach Z', + latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-with-match-2-build', status: 'running', resources: [ { + ...MockWorkspaceResource, id: 'workspace-with-match-2-resource', - agents: [{ id: 'test-workspace-agent', status: 'connected' }], + agents: [ + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent', + status: 'connected', + }, + ], }, ], }, @@ -49,19 +83,31 @@ export const mockWorkspaceWithMatch2: Workspace = { * cleanedRepoUrl */ export const mockWorkspaceNoMatch: Workspace = { + ...MockWorkspace, id: 'workspace-no-match', name: 'No-match', template_icon: `${mockBackstageApiEndpoint}/emojis/star.svg`, owner_name: 'homestar runner', + latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-no-match-build', status: 'stopped', resources: [ { + ...MockWorkspaceResource, id: 'workspace-no-match-resource', agents: [ - { id: 'test-workspace-agent-a', status: 'disconnected' }, - { id: 'test-workspace-agent-b', status: 'timeout' }, + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-a', + status: 'disconnected', + }, + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-b', + status: 'timeout', + }, ], }, ], @@ -72,17 +118,22 @@ export const mockWorkspaceNoMatch: Workspace = { * A workspace with no build parameters whatsoever */ export const mockWorkspaceNoParameters: Workspace = { + ...MockWorkspace, id: 'workspace-no-parameters', name: 'No-parameters', template_icon: `${mockBackstageApiEndpoint}/emojis/cheese.png`, owner_name: 'The Cheat', latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-no-parameters-build', status: 'running', resources: [ { + ...MockWorkspaceResource, id: 'workspace-no-parameters-resource', - agents: [{ id: 'test-workspace-c', status: 'timeout' }], + agents: [ + { ...MockWorkspaceAgent, id: 'test-workspace-c', status: 'timeout' }, + ], }, ], }, diff --git a/plugins/backstage-plugin-coder/src/testHelpers/server.ts b/plugins/backstage-plugin-coder/src/testHelpers/server.ts index 69fe816a..bacd3f43 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/server.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/server.ts @@ -11,19 +11,18 @@ import { setupServer } from 'msw/node'; /* eslint-enable @backstage/no-undeclared-imports */ import { + mockUserWithProxyUrls, mockWorkspacesList, mockWorkspacesListForRepoSearch, -} from './mockCoderAppData'; +} from './mockCoderPluginData'; import { - mockBackstageAssetsEndpoint, mockBearerToken, mockCoderAuthToken, mockCoderWorkspacesConfig, mockBackstageApiEndpoint as root, } from './mockBackstageData'; -import type { WorkspacesResponse } from '../typesConstants'; import { CODER_AUTH_HEADER_KEY } from '../api/CoderClient'; -import { User } from '../typesConstants'; +import type { User, WorkspacesResponse } from '../api/vendoredSdk'; type RestResolver = ResponseResolver< RestRequest, @@ -83,7 +82,6 @@ export function wrappedGet( export const mockServerEndpoints = { workspaces: `${root}/workspaces`, authenticatedUser: `${root}/users/me`, - workspaceBuildParameters: `${root}/workspacebuilds/:workspaceBuildId/parameters`, } as const satisfies Record; const mainTestHandlers: readonly RestHandler[] = [ @@ -93,7 +91,7 @@ const mainTestHandlers: readonly RestHandler[] = [ `param:"\\w+?=${repoUrl.replace('/', '\\/')}"`, ); - const queryText = String(req.url.searchParams.get('q')); + const queryText = String(req.url.searchParams.get('q') ?? ''); const requestContainsRepoInfo = paramMatcherRe.test(queryText); const baseWorkspaces = requestContainsRepoInfo @@ -129,14 +127,7 @@ const mainTestHandlers: readonly RestHandler[] = [ // This is the dummy request used to verify a user's auth status wrappedGet(mockServerEndpoints.authenticatedUser, (_, res, ctx) => { - return res( - ctx.status(200), - ctx.json({ - id: '1', - avatar_url: `${mockBackstageAssetsEndpoint}/blueberry.png`, - username: 'blueberry', - }), - ); + return res(ctx.status(200), ctx.json(mockUserWithProxyUrls)); }), ]; diff --git a/plugins/backstage-plugin-coder/src/typesConstants.ts b/plugins/backstage-plugin-coder/src/typesConstants.ts index 76551f89..986696bd 100644 --- a/plugins/backstage-plugin-coder/src/typesConstants.ts +++ b/plugins/backstage-plugin-coder/src/typesConstants.ts @@ -1,14 +1,3 @@ -import { - type Output, - array, - number, - object, - string, - union, - literal, - optional, -} from 'valibot'; - export type ReadonlyJsonValue = | string | number @@ -30,80 +19,6 @@ export const CODER_API_REF_ID_PREFIX = 'backstage-plugin-coder'; export const DEFAULT_CODER_DOCS_LINK = 'https://coder.com/docs/v2/latest'; -export const workspaceAgentStatusSchema = union([ - literal('connected'), - literal('connecting'), - literal('disconnected'), - literal('timeout'), -]); - -export const workspaceAgentSchema = object({ - id: string(), - status: workspaceAgentStatusSchema, -}); - -export const workspaceResourceSchema = object({ - id: string(), - agents: optional(array(workspaceAgentSchema)), -}); - -export const workspaceStatusSchema = union([ - literal('canceled'), - literal('canceling'), - literal('deleted'), - literal('deleting'), - literal('failed'), - literal('pending'), - literal('running'), - literal('starting'), - literal('stopped'), - literal('stopping'), -]); - -export const workspaceBuildSchema = object({ - id: string(), - resources: array(workspaceResourceSchema), - status: workspaceStatusSchema, -}); - -export const workspaceSchema = object({ - id: string(), - name: string(), - template_icon: string(), - owner_name: string(), - latest_build: workspaceBuildSchema, -}); - -export const workspacesResponseSchema = object({ - count: number(), - workspaces: array(workspaceSchema), -}); - -export type WorkspaceAgentStatus = Output; -export type WorkspaceAgent = Output; -export type WorkspaceResource = Output; -export type WorkspaceStatus = Output; -export type WorkspaceBuild = Output; -export type Workspace = Output; -export type WorkspacesResponse = Output; - -export type WorkspacesRequest = Readonly<{ - after_id?: string; - limit?: number; - offset?: number; - q?: string; -}>; - -// This is actually the MinimalUser type from Coder core (User extends from -// ReducedUser, which extends from MinimalUser). Don't need all the properties -// until we roll out full SDK support, so going with the least privileged -// type definition for now -export type User = Readonly<{ - id: string; - username: string; - avatar_url: string; -}>; - /** * 2024-05-22 - While this isn't documented anywhere, TanStack Query defaults to * retrying a failed API request 3 times before exposing an error to the UI diff --git a/plugins/backstage-plugin-coder/src/utils/workspaces.ts b/plugins/backstage-plugin-coder/src/utils/workspaces.ts index c36b6d4b..f9317a97 100644 --- a/plugins/backstage-plugin-coder/src/utils/workspaces.ts +++ b/plugins/backstage-plugin-coder/src/utils/workspaces.ts @@ -1,4 +1,4 @@ -import type { Workspace, WorkspaceAgentStatus } from '../typesConstants'; +import { Workspace, WorkspaceAgentStatus } from '../api/vendoredSdk'; export function getWorkspaceAgentStatuses( workspace: Workspace, From 251214e7b05d77c2971866f3da118d833126f762 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 4 Jun 2024 10:55:21 -0400 Subject: [PATCH 4/9] feat(Coder plugin): expose Coder SDK to Backstage end-users (#132) * 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 * fix: add extra helpers to useCoderSdk * 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: update reversion --- .../src/api/queryOptions.ts | 5 +- .../src/api/vendoredSdk/api/api.ts | 2 +- .../CoderProvider/CoderAuthProvider.tsx | 84 +++++- .../CoderProvider/CoderProvider.test.tsx | 1 + .../CoderProvider/CoderProvider.tsx | 5 +- .../components/CoderWorkspacesCard/Root.tsx | 2 +- .../useCoderWorkspacesQuery.test.ts | 7 +- .../useCoderWorkspacesQuery.ts | 8 +- .../src/hooks/reactQueryWrappers.test.tsx | 248 ++++++++++++++++++ .../src/hooks/reactQueryWrappers.ts | 157 +++++++++++ plugins/backstage-plugin-coder/src/plugin.ts | 6 + .../src/testHelpers/setup.tsx | 23 +- 12 files changed, 522 insertions(+), 26 deletions(-) rename plugins/backstage-plugin-coder/src/{hooks => components/CoderWorkspacesCard}/useCoderWorkspacesQuery.test.ts (91%) rename plugins/backstage-plugin-coder/src/{hooks => components/CoderWorkspacesCard}/useCoderWorkspacesQuery.ts (66%) create mode 100644 plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx create mode 100644 plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts diff --git a/plugins/backstage-plugin-coder/src/api/queryOptions.ts b/plugins/backstage-plugin-coder/src/api/queryOptions.ts index 4e55861d..6bfbd800 100644 --- a/plugins/backstage-plugin-coder/src/api/queryOptions.ts +++ b/plugins/backstage-plugin-coder/src/api/queryOptions.ts @@ -4,7 +4,10 @@ import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; import type { BackstageCoderSdk } from './CoderClient'; import type { CoderAuth } from '../components/CoderProvider'; -export const CODER_QUERY_KEY_PREFIX = 'coder-backstage-plugin'; +// Making the type more broad to hide some implementation details from the end +// user; the prefix should be treated as an opaque string we can change whenever +// we want +export const CODER_QUERY_KEY_PREFIX = 'coder-backstage-plugin' as string; // Defined here and not in CoderAuthProvider.ts to avoid circular dependency // issues diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts index e0eafd1d..bf293267 100644 --- a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts @@ -312,7 +312,7 @@ type RestartWorkspaceParameters = Readonly<{ export type DeleteWorkspaceOptions = Pick< TypesGen.CreateWorkspaceBuildRequest, - 'log_level' & 'orphan' + 'log_level' | 'orphan' >; type Claims = { diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index 664bb311..33b5bc0a 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -1,4 +1,5 @@ import React, { + type FC, type PropsWithChildren, createContext, useCallback, @@ -136,10 +137,16 @@ function useAuthState(): CoderAuth { return () => window.clearTimeout(distrustTimeoutId); }, [authState.status]); + const isAuthenticated = validAuthStatuses.includes(authState.status); + // Sets up subscription to spy on potentially-expired tokens. Can't do this // outside React because we let the user connect their own queryClient const queryClient = useQueryClient(); useEffect(() => { + if (!isAuthenticated) { + return undefined; + } + // Pseudo-mutex; makes sure that if we get a bunch of errors, only one // revalidation will be processed at a time let isRevalidatingToken = false; @@ -163,7 +170,7 @@ function useAuthState(): CoderAuth { const queryCache = queryClient.getQueryCache(); const unsubscribe = queryCache.subscribe(revalidateTokenOnError); return unsubscribe; - }, [queryClient]); + }, [queryClient, isAuthenticated]); const registerNewToken = useCallback((newToken: string) => { if (newToken !== '') { @@ -179,7 +186,7 @@ function useAuthState(): CoderAuth { return { ...authState, - isAuthenticated: validAuthStatuses.includes(authState.status), + isAuthenticated, registerNewToken, ejectToken, }; @@ -607,24 +614,75 @@ export const dummyTrackComponent: TrackComponent = () => { }; }; +export type FallbackAuthInputBehavior = 'restrained' | 'assertive' | 'hidden'; +type AuthFallbackProvider = FC< + Readonly< + PropsWithChildren<{ + isAuthenticated: boolean; + }> + > +>; + +// Matches each behavior for the fallback auth UI to a specific provider. This +// is screwy code, but by doing this, we ensure that if the user chooses not to +// have a dynamic auth fallback UI, their app will have far less tracking logic, +// meaning less performance overhead and fewer re-renders from something the +// user isn't even using +const fallbackProviders = { + hidden: ({ children }) => ( + + {children} + + ), + + assertive: ({ children, isAuthenticated }) => ( + // Don't need the live version of the tracker function if we're always + // going to be showing the fallback auth input no matter what + + {children} + {!isAuthenticated && } + + ), + + // Have to give function a name to satisfy ES Lint (rules of hooks) + restrained: function Restrained({ children, isAuthenticated }) { + const { hasNoAuthInputs, trackComponent } = useAuthFallbackState(); + const needFallbackUi = !isAuthenticated && hasNoAuthInputs; + + return ( + <> + + {children} + + + {needFallbackUi && ( + + + + )} + + ); + }, +} as const satisfies Record; + +export type CoderAuthProviderProps = Readonly< + PropsWithChildren<{ + fallbackAuthUiMode?: FallbackAuthInputBehavior; + }> +>; + export function CoderAuthProvider({ children, -}: Readonly>) { + fallbackAuthUiMode = 'restrained', +}: CoderAuthProviderProps) { const authState = useAuthState(); - const { hasNoAuthInputs, trackComponent } = useAuthFallbackState(); - const needFallbackUi = !authState.isAuthenticated && hasNoAuthInputs; + const AuthFallbackProvider = fallbackProviders[fallbackAuthUiMode]; return ( - + {children} - - - {needFallbackUi && ( - - - - )} + ); } diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx index 73acc13c..382917d8 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx @@ -86,6 +86,7 @@ describe(`${CoderProvider.name}`, () => { {children} diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx index 1b825404..fd562851 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx @@ -46,12 +46,15 @@ export const CoderProvider = ({ children, appConfig, queryClient = defaultClient, + fallbackAuthUiMode = 'restrained', }: CoderProviderProps) => { return ( - {children} + + {children} + diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx index 452f0a9c..5814d55b 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx @@ -16,7 +16,7 @@ import { type CoderWorkspacesConfig, } from '../../hooks/useCoderWorkspacesConfig'; import type { Workspace } from '../../api/vendoredSdk'; -import { useCoderWorkspacesQuery } from '../../hooks/useCoderWorkspacesQuery'; +import { useCoderWorkspacesQuery } from './useCoderWorkspacesQuery'; import { CoderAuthFormCardWrapper } from '../CoderAuthFormCardWrapper'; export type WorkspacesQuery = UseQueryResult; diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.test.ts similarity index 91% rename from plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts rename to plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.test.ts index 49535619..9f22cf94 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.test.ts @@ -1,12 +1,11 @@ import { waitFor } from '@testing-library/react'; import { useCoderWorkspacesQuery } from './useCoderWorkspacesQuery'; - -import { renderHookAsCoderEntity } from '../testHelpers/setup'; -import { mockCoderWorkspacesConfig } from '../testHelpers/mockBackstageData'; +import { renderHookAsCoderEntity } from '../../testHelpers/setup'; +import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData'; import { mockWorkspaceNoParameters, mockWorkspacesList, -} from '../testHelpers/mockCoderPluginData'; +} from '../../testHelpers/mockCoderPluginData'; beforeAll(() => { jest.useFakeTimers(); diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts similarity index 66% rename from plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts rename to plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts index 63b4f2f7..5f82e6b7 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts @@ -1,8 +1,8 @@ import { useQuery } from '@tanstack/react-query'; -import { workspaces, workspacesByRepo } from '../api/queryOptions'; -import type { CoderWorkspacesConfig } from './useCoderWorkspacesConfig'; -import { useCoderSdk } from './useCoderSdk'; -import { useInternalCoderAuth } from '../components/CoderProvider'; +import { workspaces, workspacesByRepo } from '../../api/queryOptions'; +import type { CoderWorkspacesConfig } from '../../hooks/useCoderWorkspacesConfig'; +import { useCoderSdk } from '../../hooks/useCoderSdk'; +import { useInternalCoderAuth } from '../../components/CoderProvider'; type QueryInput = Readonly<{ coderQuery: string; diff --git a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx new file mode 100644 index 00000000..83309a08 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx @@ -0,0 +1,248 @@ +import React from 'react'; +import { act, renderHook, waitFor } from '@testing-library/react'; +import type { + QueryClient, + QueryKey, + UseQueryResult, +} from '@tanstack/react-query'; +import { + type UseCoderQueryOptions, + useCoderQuery, + CoderQueryFunction, +} from './reactQueryWrappers'; +import { + type CoderAuth, + CoderProvider, + useEndUserCoderAuth, +} from '../components/CoderProvider'; +import { + getMockApiList, + mockAppConfig, + mockCoderAuthToken, +} from '../testHelpers/mockBackstageData'; +import { + createInvertedPromise, + getMockQueryClient, +} from '../testHelpers/setup'; +import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; +import { CODER_QUERY_KEY_PREFIX } from '../api/queryOptions'; +import { mockWorkspacesList } from '../testHelpers/mockCoderPluginData'; + +type RenderUseQueryOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Readonly<{ + authenticateOnMount?: boolean; + queryClient?: QueryClient; + queryOptions: UseCoderQueryOptions; +}>; + +async function renderCoderQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>(options: RenderUseQueryOptions) { + const { + queryOptions, + authenticateOnMount = true, + queryClient = getMockQueryClient(), + } = options; + + let latestRegisterNewToken!: CoderAuth['registerNewToken']; + let latestEjectToken!: CoderAuth['ejectToken']; + const AuthEscapeHatch = () => { + const auth = useEndUserCoderAuth(); + latestRegisterNewToken = auth.registerNewToken; + latestEjectToken = auth.ejectToken; + + return null; + }; + + type Result = UseQueryResult; + const renderOutput = renderHook( + newOptions => useCoderQuery(newOptions), + { + initialProps: queryOptions, + wrapper: ({ children }) => { + const mainMarkup = ( + + + {children} + + + + ); + + return wrapInTestApp(mainMarkup) as unknown as typeof mainMarkup; + }, + }, + ); + + await waitFor(() => expect(renderOutput.result.current).not.toBeNull()); + + const registerMockToken = () => { + return act(() => latestRegisterNewToken(mockCoderAuthToken)); + }; + + const ejectToken = () => { + return act(() => latestEjectToken()); + }; + + if (authenticateOnMount) { + registerMockToken(); + } + + return { ...renderOutput, registerMockToken, ejectToken }; +} + +describe(`${useCoderQuery.name}`, () => { + /** + * Really wanted to make mock components for each test case, to simulate some + * of the steps of using the hook as an actual end-user, but the setup steps + * got to be a bit much, just because of all the dependencies to juggle. + * + * @todo Add a new describe block with custom components to mirror some + * example user flows + */ + describe('Hook functionality', () => { + it('Disables requests while user is not authenticated', async () => { + const { result, registerMockToken, ejectToken } = await renderCoderQuery({ + authenticateOnMount: false, + queryOptions: { + queryKey: ['workspaces'], + queryFn: ({ sdk }) => sdk.getWorkspaces({ q: 'owner:me' }), + select: response => response.workspaces, + }, + }); + + expect(result.current.isLoading).toBe(true); + registerMockToken(); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + expect(result.current.data?.length).toBeGreaterThan(0); + }); + + ejectToken(); + await waitFor(() => expect(result.current.isLoading).toBe(true)); + }); + + it("Automatically prefixes queryKey with the global Coder query key prefix if it isn't already there", async () => { + // Have to escape out the key because useQuery doesn't expose any way to + // access the key after it's been processed into a query result object + let processedQueryKey: QueryKey | undefined = undefined; + + const queryFnWithEscape: CoderQueryFunction = ({ queryKey }) => { + processedQueryKey = queryKey; + return Promise.resolve(mockWorkspacesList); + }; + + // Verify that key is updated if the prefix isn't already there + const { unmount } = await renderCoderQuery({ + queryOptions: { + queryKey: ['blah'], + queryFn: queryFnWithEscape, + }, + }); + + await waitFor(() => { + expect(processedQueryKey).toEqual([ + CODER_QUERY_KEY_PREFIX, + 'blah', + ]); + }); + + // Unmounting shouldn't really be necessary, but it helps guarantee that + // there's never any risks of states messing with each other + unmount(); + + // Verify that the key is unchanged if the prefix is already present + await renderCoderQuery({ + queryOptions: { + queryKey: [CODER_QUERY_KEY_PREFIX, 'nah'], + queryFn: queryFnWithEscape, + }, + }); + + await waitFor(() => { + expect(processedQueryKey).toEqual([ + CODER_QUERY_KEY_PREFIX, + 'nah', + ]); + }); + }); + + it('Disables everything when the user unlinks their access token', async () => { + const { result, ejectToken } = await renderCoderQuery({ + queryOptions: { + queryKey: ['workspaces'], + queryFn: () => Promise.resolve(mockWorkspacesList), + }, + }); + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining>({ + isSuccess: true, + isPaused: false, + data: mockWorkspacesList, + }), + ); + }); + + ejectToken(); + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining>({ + isLoading: true, + isPaused: false, + data: undefined, + }), + ); + }); + }); + + /** + * In case the title isn't clear (had to rewrite it a bunch), the flow is: + * + * 1. User gets authenticated + * 2. User makes a request that will fail + * 3. Before the request comes back, the user revokes their authentication + * 4. The failed request comes back, which would normally add error state, + * and kick off a bunch of retry logic for React Query + * 5. But the hook should tell the Query Client NOT retry the request + * because the user is no longer authenticated + */ + it('Will not retry a request if it gets sent out while the user is authenticated, but then fails after the user revokes authentication', async () => { + const { promise, reject } = createInvertedPromise(); + const queryFn = jest.fn(() => promise); + + const { ejectToken } = await renderCoderQuery({ + queryOptions: { + queryFn, + queryKey: ['blah'], + + // From the end user's perspective, the query should always retry, but + // the hook should override that when the user isn't authenticated + retry: true, + }, + }); + + await waitFor(() => expect(queryFn).toHaveBeenCalled()); + ejectToken(); + + queryFn.mockRestore(); + act(() => reject(new Error("Don't feel like giving you data today"))); + expect(queryFn).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts new file mode 100644 index 00000000..6dff0240 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts @@ -0,0 +1,157 @@ +/** + * @file Defines a couple of wrappers over React Query/Tanstack Query that make + * it easier to use the Coder SDK within UI logic. + * + * These hooks are designed 100% for end-users, and should not be used + * internally. Use useEndUserCoderAuth when working with auth logic within these + * hooks. + * + * --- + * @todo 2024-05-28 - This isn't fully complete until we have an equivalent + * wrapper for useMutation, and have an idea of how useCoderQuery and + * useCoderMutation can be used together. + * + * Making the useMutation wrapper shouldn't be hard, but you want some good + * integration tests to verify that the two hooks can satisfy common user flows. + */ +import { + type QueryFunctionContext, + type QueryKey, + type UseQueryOptions, + type UseQueryResult, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; +import { DEFAULT_TANSTACK_QUERY_RETRY_COUNT } from '../typesConstants'; +import { useEndUserCoderAuth } from '../components/CoderProvider'; +import { CODER_QUERY_KEY_PREFIX } from '../api/queryOptions'; +import { useCoderSdk } from './useCoderSdk'; +import type { BackstageCoderSdk } from '../api/CoderClient'; + +export type CoderQueryFunctionContext = + QueryFunctionContext & { + sdk: BackstageCoderSdk; + }; + +export type CoderQueryFunction< + T = unknown, + TQueryKey extends QueryKey = QueryKey, +> = (context: CoderQueryFunctionContext) => Promise; + +export type UseCoderQueryOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Omit< + UseQueryOptions, + // queryFn omitted so that a custom version can be patched in; all other + // properties omitted because they are officially deprecated in React Query v4 + // and outright removed in v5. Want better future-proofing + 'queryFn' | 'isDataEqual' | 'onError' | 'onSuccess' | 'onSettled' +> & { + queryFn: CoderQueryFunction; +}; + +export function useCoderQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + queryOptions: UseCoderQueryOptions, +): UseQueryResult { + const queryClient = useQueryClient(); + const { isAuthenticated } = useEndUserCoderAuth(); + const sdk = useCoderSdk(); + + let patchedQueryKey = queryOptions.queryKey; + if ( + patchedQueryKey === undefined || + patchedQueryKey[0] !== CODER_QUERY_KEY_PREFIX + ) { + const baseKey = + queryOptions.queryKey ?? queryClient.defaultQueryOptions().queryKey; + + if (baseKey === undefined) { + throw new Error('No queryKey value provided to useCoderQuery'); + } + + patchedQueryKey = [ + CODER_QUERY_KEY_PREFIX, + ...baseKey, + ] as QueryKey as TQueryKey; + } + + type Options = UseQueryOptions; + const patchedOptions: Options = { + ...queryOptions, + queryKey: patchedQueryKey, + enabled: isAuthenticated && (queryOptions.enabled ?? true), + keepPreviousData: + isAuthenticated && (queryOptions.keepPreviousData ?? false), + refetchIntervalInBackground: + isAuthenticated && (queryOptions.refetchIntervalInBackground ?? false), + + queryFn: async context => { + if (!isAuthenticated) { + throw new Error('Cannot complete request - user is not authenticated'); + } + + return queryOptions.queryFn({ ...context, sdk }); + }, + + refetchInterval: (data, query) => { + if (!isAuthenticated) { + return false; + } + + const externalRefetchInterval = queryOptions.refetchInterval; + if (typeof externalRefetchInterval !== 'function') { + return externalRefetchInterval ?? false; + } + + return externalRefetchInterval(data, query); + }, + + refetchOnMount: query => { + if (!isAuthenticated) { + return false; + } + + const externalRefetchOnMount = queryOptions.refetchOnMount; + if (typeof externalRefetchOnMount !== 'function') { + return externalRefetchOnMount ?? true; + } + + return externalRefetchOnMount(query); + }, + + retry: (failureCount, error) => { + if (!isAuthenticated) { + return false; + } + + const externalRetry = queryOptions.retry; + if (typeof externalRetry === 'number') { + const normalized = Number.isInteger(externalRetry) + ? Math.max(1, externalRetry) + : DEFAULT_TANSTACK_QUERY_RETRY_COUNT; + + return failureCount < normalized; + } + + if (typeof externalRetry !== 'function') { + // Could use the nullish coalescing operator here, but Prettier made the + // output hard to read + return externalRetry + ? externalRetry + : failureCount < DEFAULT_TANSTACK_QUERY_RETRY_COUNT; + } + + return externalRetry(failureCount, error); + }, + }; + + return useQuery(patchedOptions); +} diff --git a/plugins/backstage-plugin-coder/src/plugin.ts b/plugins/backstage-plugin-coder/src/plugin.ts index 2aaaab89..904b7705 100644 --- a/plugins/backstage-plugin-coder/src/plugin.ts +++ b/plugins/backstage-plugin-coder/src/plugin.ts @@ -192,6 +192,12 @@ export { useWorkspacesCardContext } from './components/CoderWorkspacesCard/Root' export { useCoderWorkspacesConfig } from './hooks/useCoderWorkspacesConfig'; export { useCoderSdk } from './hooks/useCoderSdk'; export { useEndUserCoderAuth as useCoderAuth } from './components/CoderProvider/CoderAuthProvider'; +export { useCoderQuery } from './hooks/reactQueryWrappers'; + +/** + * General constants + */ +export { CODER_QUERY_KEY_PREFIX } from './api/queryOptions'; /** * All custom types diff --git a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx index 86ceedcb..cc8c67ad 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx +++ b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx @@ -106,7 +106,7 @@ export function getMockQueryClient(): QueryClient { } type MockAuthProps = Readonly< - CoderProviderProps & { + Omit & { auth?: CoderAuth; /** @@ -221,3 +221,24 @@ export async function renderInCoderEnvironment({ await waitFor(() => expect(loadingIndicator).not.toBeInTheDocument()); return renderOutput; } + +type InvertedPromiseResult = Readonly<{ + promise: Promise; + resolve: (value: TData) => void; + reject: (errorReason: TError) => void; +}>; + +export function createInvertedPromise< + TData = unknown, + TError = Error, +>(): InvertedPromiseResult { + let resolve!: (value: TData) => void; + let reject!: (error: TError) => void; + + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + return { promise, resolve, reject }; +} From 80d6858867de084d6c706db186cfab475214f59d Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 17 Jun 2024 15:50:28 -0400 Subject: [PATCH 5/9] chore(Coder plugin): Create guide for working with the Coder SDK (#133) * 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 --- plugins/backstage-plugin-coder/docs/README.md | 25 +- .../docs/{ => api-reference}/catalog-info.md | 0 .../docs/{ => api-reference}/components.md | 0 .../docs/{ => api-reference}/hooks.md | 0 .../docs/{ => api-reference}/types.md | 0 .../docs/guides/coder-api-advanced.md | 72 +++++ .../docs/guides/coder-api.md | 262 ++++++++++++++++++ .../screenshots/auth-fallback.png | Bin 0 -> 397274 bytes .../src/api/CoderClient.test.ts | 26 +- .../src/api/CoderClient.ts | 40 +-- .../src/api/UrlSync.test.ts | 4 +- .../src/api/queryOptions.ts | 12 +- .../src/api/vendoredSdk/api/api.ts | 2 +- .../src/api/vendoredSdk/index.ts | 6 +- .../CoderAuthForm/CoderAuthDistrustedForm.tsx | 2 +- .../CoderAuthForm/CoderAuthForm.test.tsx | 16 +- .../CoderAuthForm/UnlinkAccountButton.tsx | 4 +- .../CoderProvider/CoderAuthProvider.tsx | 24 +- .../CoderProvider/CoderProvider.test.tsx | 13 +- .../CoderProvider/CoderProvider.tsx | 2 +- .../ExtraActionsButton.test.tsx | 9 +- .../ExtraActionsButton.tsx | 4 +- .../useCoderWorkspacesQuery.ts | 8 +- .../src/hooks/reactQueryWrappers.test.tsx | 39 +-- .../src/hooks/reactQueryWrappers.ts | 12 +- .../hooks/{useCoderSdk.ts => useCoderApi.ts} | 13 +- .../src/hooks/useUrlSync.test.tsx | 6 +- plugins/backstage-plugin-coder/src/plugin.ts | 17 +- .../src/testHelpers/mockBackstageData.ts | 23 +- .../src/testHelpers/setup.tsx | 11 +- 30 files changed, 514 insertions(+), 138 deletions(-) rename plugins/backstage-plugin-coder/docs/{ => api-reference}/catalog-info.md (100%) rename plugins/backstage-plugin-coder/docs/{ => api-reference}/components.md (100%) rename plugins/backstage-plugin-coder/docs/{ => api-reference}/hooks.md (100%) rename plugins/backstage-plugin-coder/docs/{ => api-reference}/types.md (100%) create mode 100644 plugins/backstage-plugin-coder/docs/guides/coder-api-advanced.md create mode 100644 plugins/backstage-plugin-coder/docs/guides/coder-api.md create mode 100644 plugins/backstage-plugin-coder/screenshots/auth-fallback.png rename plugins/backstage-plugin-coder/src/hooks/{useCoderSdk.ts => useCoderApi.ts} (51%) diff --git a/plugins/backstage-plugin-coder/docs/README.md b/plugins/backstage-plugin-coder/docs/README.md index 1aac4a05..95019233 100644 --- a/plugins/backstage-plugin-coder/docs/README.md +++ b/plugins/backstage-plugin-coder/docs/README.md @@ -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. diff --git a/plugins/backstage-plugin-coder/docs/catalog-info.md b/plugins/backstage-plugin-coder/docs/api-reference/catalog-info.md similarity index 100% rename from plugins/backstage-plugin-coder/docs/catalog-info.md rename to plugins/backstage-plugin-coder/docs/api-reference/catalog-info.md diff --git a/plugins/backstage-plugin-coder/docs/components.md b/plugins/backstage-plugin-coder/docs/api-reference/components.md similarity index 100% rename from plugins/backstage-plugin-coder/docs/components.md rename to plugins/backstage-plugin-coder/docs/api-reference/components.md diff --git a/plugins/backstage-plugin-coder/docs/hooks.md b/plugins/backstage-plugin-coder/docs/api-reference/hooks.md similarity index 100% rename from plugins/backstage-plugin-coder/docs/hooks.md rename to plugins/backstage-plugin-coder/docs/api-reference/hooks.md diff --git a/plugins/backstage-plugin-coder/docs/types.md b/plugins/backstage-plugin-coder/docs/api-reference/types.md similarity index 100% rename from plugins/backstage-plugin-coder/docs/types.md rename to plugins/backstage-plugin-coder/docs/api-reference/types.md diff --git a/plugins/backstage-plugin-coder/docs/guides/coder-api-advanced.md b/plugins/backstage-plugin-coder/docs/guides/coder-api-advanced.md new file mode 100644 index 00000000..fb90ebe6 --- /dev/null +++ b/plugins/backstage-plugin-coder/docs/guides/coder-api-advanced.md @@ -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. + +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 + + + +``` + +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(); + + + +; + +// 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
Main component content
; +} +``` diff --git a/plugins/backstage-plugin-coder/docs/guides/coder-api.md b/plugins/backstage-plugin-coder/docs/guides/coder-api.md new file mode 100644 index 00000000..04e8d10d --- /dev/null +++ b/plugins/backstage-plugin-coder/docs/guides/coder-api.md @@ -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) => { + coderAuth.registerNewToken(sessionToken); + setSessionTokenDraft(''); + }; + + return ( + + + + ); + } + ``` + +- `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 ( + + ); +} +``` + +## 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 &&

Loading…

} + {auditLogsQuery.error instanceof Error && ( +

Encountered the following error: {auditLogsQuery.error.message}

+ )} + + {auditLogsQuery.data !== undefined && ( +
    + {auditLogsQuery.data.audit_logs.map(log => ( +
  • {log.description}
  • + ))} +
+ )} + + ); +} +``` + +## 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) => { + 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 && ( +

+ Workspace {createWorkspaceMutation.data.name} created successfully! +

+ )} + +
+
+ Required fields + + +
+ + +
+ + ); +} +``` diff --git a/plugins/backstage-plugin-coder/screenshots/auth-fallback.png b/plugins/backstage-plugin-coder/screenshots/auth-fallback.png new file mode 100644 index 0000000000000000000000000000000000000000..d5b817ccdfa7a3512dd3d73602661eb48906c664 GIT binary patch literal 397274 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

$WeS|F$MO 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 Y(zoV { +describe(`${CoderClientWrapper.name}`, () => { describe('syncToken functionality', () => { it('Will load the provided token into the client if it is valid', async () => { - const client = new CoderClient({ apis: getConstructorApis() }); + const client = new CoderClientWrapper({ apis: getConstructorApis() }); const syncResult = await client.syncToken(mockCoderAuthToken); expect(syncResult).toBe(true); @@ -50,12 +50,12 @@ describe(`${CoderClient.name}`, () => { }), ); - await client.sdk.getAuthenticatedUser(); + await client.api.getAuthenticatedUser(); expect(serverToken).toBe(mockCoderAuthToken); }); it('Will NOT load the provided token into the client if it is invalid', async () => { - const client = new CoderClient({ apis: getConstructorApis() }); + const client = new CoderClientWrapper({ apis: getConstructorApis() }); const syncResult = await client.syncToken('Definitely not valid'); expect(syncResult).toBe(false); @@ -68,12 +68,12 @@ describe(`${CoderClient.name}`, () => { }), ); - await client.sdk.getAuthenticatedUser(); + await client.api.getAuthenticatedUser(); expect(serverToken).toBe(null); }); it('Will propagate any other error types to the caller', async () => { - const client = new CoderClient({ + const client = new CoderClientWrapper({ // Setting the timeout to 0 will make requests instantly fail from the // next microtask queue tick requestTimeoutMs: 0, @@ -96,13 +96,13 @@ describe(`${CoderClient.name}`, () => { }); }); - // Eventually the Coder SDK is going to get too big to test every single + // Eventually the Coder API is going to get too big to test every single // function. Focus tests on the functionality specifically being patched in // for Backstage - describe('Coder SDK', () => { + describe('Coder API', () => { it('Will remap all workspace icon URLs to use the proxy URL if necessary', async () => { const apis = getConstructorApis(); - const client = new CoderClient({ + const client = new CoderClientWrapper({ apis, initialToken: mockCoderAuthToken, }); @@ -126,7 +126,7 @@ describe(`${CoderClient.name}`, () => { }), ); - const { workspaces } = await client.sdk.getWorkspaces({ + const { workspaces } = await client.api.getWorkspaces({ q: 'owner:me', limit: 0, }); @@ -142,12 +142,12 @@ describe(`${CoderClient.name}`, () => { }); it('Lets the user search for workspaces by repo URL', async () => { - const client = new CoderClient({ + const client = new CoderClientWrapper({ initialToken: mockCoderAuthToken, apis: getConstructorApis(), }); - const { workspaces } = await client.sdk.getWorkspacesByRepo( + const { workspaces } = await client.api.getWorkspacesByRepo( { q: 'owner:me' }, mockCoderWorkspacesConfig, ); diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.ts index 4c5333dd..c760f1d2 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.ts @@ -7,23 +7,23 @@ import { CODER_API_REF_ID_PREFIX } from '../typesConstants'; import type { UrlSync } from './UrlSync'; import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; import { - type CoderSdk, + type CoderApi, type User, type Workspace, type WorkspacesRequest, type WorkspacesResponse, - makeCoderSdk, + createCoderApi, } from './vendoredSdk'; export const CODER_AUTH_HEADER_KEY = 'Coder-Session-Token'; const DEFAULT_REQUEST_TIMEOUT_MS = 20_000; /** - * A version of the main Coder SDK API, with additional Backstage-specific + * A version of the main Coder API, with additional Backstage-specific * methods and properties. */ -export type BackstageCoderSdk = Readonly< - CoderSdk & { +export type BackstageCoderApi = Readonly< + CoderApi & { getWorkspacesByRepo: ( request: WorkspacesRequest, config: CoderWorkspacesConfig, @@ -31,8 +31,8 @@ export type BackstageCoderSdk = Readonly< } >; -type CoderClientApi = Readonly<{ - sdk: BackstageCoderSdk; +type CoderClientWrapperApi = Readonly<{ + api: BackstageCoderApi; /** * Validates a new token, and loads it only if it is valid. @@ -75,7 +75,7 @@ type RequestInterceptor = ( config: RequestConfig, ) => RequestConfig | Promise; -export class CoderClient implements CoderClientApi { +export class CoderClientWrapper implements CoderClientWrapperApi { private readonly urlSync: UrlSync; private readonly identityApi: IdentityApi; @@ -84,7 +84,7 @@ export class CoderClient implements CoderClientApi { private readonly trackedEjectionIds: Set; private loadedSessionToken: string | undefined; - readonly sdk: BackstageCoderSdk; + readonly api: BackstageCoderApi; constructor(inputs: ConstructorInputs) { const { @@ -100,7 +100,7 @@ export class CoderClient implements CoderClientApi { this.cleanupController = new AbortController(); this.trackedEjectionIds = new Set(); - this.sdk = this.createBackstageCoderSdk(); + this.api = this.createBackstageCoderApi(); this.addBaseRequestInterceptors(); } @@ -108,7 +108,7 @@ export class CoderClient implements CoderClientApi { requestInterceptor: RequestInterceptor, errorInterceptor?: (error: unknown) => unknown, ): number { - const axios = this.sdk.getAxiosInstance(); + const axios = this.api.getAxiosInstance(); const ejectionId = axios.interceptors.request.use( requestInterceptor, errorInterceptor, @@ -121,7 +121,7 @@ export class CoderClient implements CoderClientApi { private removeRequestInterceptorById(ejectionId: number): boolean { // Even if we somehow pass in an ID that hasn't been associated with the // Axios instance, that's a noop. No harm in calling method no matter what - const axios = this.sdk.getAxiosInstance(); + const axios = this.api.getAxiosInstance(); axios.interceptors.request.eject(ejectionId); if (!this.trackedEjectionIds.has(ejectionId)) { @@ -181,11 +181,11 @@ export class CoderClient implements CoderClientApi { this.addRequestInterceptor(baseRequestInterceptor, baseErrorInterceptor); } - private createBackstageCoderSdk(): BackstageCoderSdk { - const baseSdk = makeCoderSdk(); + private createBackstageCoderApi(): BackstageCoderApi { + const baseApi = createCoderApi(); - const getWorkspaces: (typeof baseSdk)['getWorkspaces'] = async request => { - const workspacesRes = await baseSdk.getWorkspaces(request); + const getWorkspaces: (typeof baseApi)['getWorkspaces'] = async request => { + const workspacesRes = await baseApi.getWorkspaces(request); const remapped = await this.remapWorkspaceIconUrls( workspacesRes.workspaces, ); @@ -214,7 +214,7 @@ export class CoderClient implements CoderClientApi { q: appendParamToQuery(request.q, key, stringUrl), }; - return baseSdk.getWorkspaces(patchedRequest); + return baseApi.getWorkspaces(patchedRequest); }), ); @@ -237,7 +237,7 @@ export class CoderClient implements CoderClientApi { }; return { - ...baseSdk, + ...baseApi, getWorkspaces, getWorkspacesByRepo, }; @@ -312,7 +312,7 @@ export class CoderClient implements CoderClientApi { // Actual request type doesn't matter; just need to make some kind of // dummy request. Should favor requests that all users have access to and // that don't require request bodies - const dummyUser = await this.sdk.getAuthenticatedUser(); + const dummyUser = await this.api.getAuthenticatedUser(); // Most of the time, we're going to trust the types returned back from the // server without doing any type-checking, but because this request does @@ -376,6 +376,6 @@ function assertValidUser(value: unknown): asserts value is User { } } -export const coderClientApiRef = createApiRef({ +export const coderClientWrapperApiRef = createApiRef({ id: `${CODER_API_REF_ID_PREFIX}.coder-client`, }); diff --git a/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts b/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts index 62001e4e..00e86a7c 100644 --- a/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts +++ b/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts @@ -5,7 +5,7 @@ import { getMockDiscoveryApi, mockBackstageAssetsEndpoint, mockBackstageUrlRoot, - mockBackstageApiEndpointWithoutSdkPath, + mockBackstageApiEndpointWithoutVersionSuffix, } from '../testHelpers/mockBackstageData'; // Tests have to assume that DiscoveryApi and ConfigApi will always be in sync, @@ -23,7 +23,7 @@ describe(`${UrlSync.name}`, () => { const cachedUrls = urlSync.getCachedUrls(); expect(cachedUrls).toEqual({ baseUrl: mockBackstageUrlRoot, - apiRoute: mockBackstageApiEndpointWithoutSdkPath, + apiRoute: mockBackstageApiEndpointWithoutVersionSuffix, assetsRoute: mockBackstageAssetsEndpoint, }); }); diff --git a/plugins/backstage-plugin-coder/src/api/queryOptions.ts b/plugins/backstage-plugin-coder/src/api/queryOptions.ts index 6bfbd800..b622e415 100644 --- a/plugins/backstage-plugin-coder/src/api/queryOptions.ts +++ b/plugins/backstage-plugin-coder/src/api/queryOptions.ts @@ -1,7 +1,7 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import type { Workspace, WorkspacesRequest } from './vendoredSdk'; import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; -import type { BackstageCoderSdk } from './CoderClient'; +import type { BackstageCoderApi } from './CoderClient'; import type { CoderAuth } from '../components/CoderProvider'; // Making the type more broad to hide some implementation details from the end @@ -47,13 +47,13 @@ function getSharedWorkspacesQueryKey(coderQuery: string) { type WorkspacesFetchInputs = Readonly<{ auth: CoderAuth; - sdk: BackstageCoderSdk; + api: BackstageCoderApi; coderQuery: string; }>; export function workspaces({ auth, - sdk, + api, coderQuery, }: WorkspacesFetchInputs): UseQueryOptions { const enabled = auth.isAuthenticated; @@ -64,7 +64,7 @@ export function workspaces({ keepPreviousData: enabled && coderQuery !== '', refetchInterval: getCoderWorkspacesRefetchInterval, queryFn: async () => { - const res = await sdk.getWorkspaces({ + const res = await api.getWorkspaces({ q: coderQuery, limit: 0, }); @@ -82,7 +82,7 @@ type WorkspacesByRepoFetchInputs = Readonly< export function workspacesByRepo({ coderQuery, - sdk, + api, auth, workspacesConfig, }: WorkspacesByRepoFetchInputs): UseQueryOptions { @@ -98,7 +98,7 @@ export function workspacesByRepo({ refetchInterval: getCoderWorkspacesRefetchInterval, queryFn: async () => { const request: WorkspacesRequest = { q: coderQuery, limit: 0 }; - const res = await sdk.getWorkspacesByRepo(request, workspacesConfig); + const res = await api.getWorkspacesByRepo(request, workspacesConfig); return res.workspaces; }, }; diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts index bf293267..6877a614 100644 --- a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts @@ -1894,7 +1894,7 @@ function getConfiguredAxiosInstance(): AxiosInstance { } else { // Do not write error logs if we are in a FE unit test. if (process.env.JEST_WORKER_ID === undefined) { - // eslint-disable-next-line no-console -- Function should never run in vendored version of SDK + // eslint-disable-next-line no-console -- Function should never run in vendored version of API console.error('CSRF token not found'); } } diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts index f8451116..18fc9eae 100644 --- a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts @@ -29,8 +29,8 @@ type PropertyToHide = // with the extra properties omitted). But because classes are wonky and exist // as both runtime values and types, it didn't seem possible, even with things // like class declarations. Making a new function is good enough for now, though -export type CoderSdk = Omit; -export function makeCoderSdk(): CoderSdk { +export type CoderApi = Omit; +export function createCoderApi(): CoderApi { const api = new Api(); - return api as CoderSdk; + return api as CoderApi; } diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthDistrustedForm.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthDistrustedForm.tsx index a37c1916..3f58804d 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthDistrustedForm.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthDistrustedForm.tsx @@ -36,7 +36,7 @@ export const CoderAuthDistrustedForm = () => {

Unable to verify token authenticity. Please check your internet - connection, or try ejecting the token. + connection, or try unlinking the token.

diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.test.tsx index 95ce2993..79b263ca 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/CoderAuthForm.test.tsx @@ -16,12 +16,12 @@ type RenderInputs = Readonly<{ }>; async function renderAuthWrapper({ authStatus }: RenderInputs) { - const ejectToken = jest.fn(); + const unlinkToken = jest.fn(); const registerNewToken = jest.fn(); const auth: CoderAuth = { ...mockAuthStates[authStatus], - ejectToken, + unlinkToken, registerNewToken, }; @@ -40,7 +40,7 @@ async function renderAuthWrapper({ authStatus }: RenderInputs) { , ); - return { ...renderOutput, ejectToken, registerNewToken }; + return { ...renderOutput, unlinkToken, registerNewToken }; } describe(`${CoderAuthForm.name}`, () => { @@ -70,18 +70,18 @@ describe(`${CoderAuthForm.name}`, () => { } }); - it('Lets the user eject the current token', async () => { - const { ejectToken } = await renderAuthWrapper({ + it('Lets the user unlink the current token', async () => { + const { unlinkToken } = await renderAuthWrapper({ authStatus: 'distrusted', }); const user = userEvent.setup(); - const ejectButton = await screen.findByRole('button', { + const unlinkButton = await screen.findByRole('button', { name: /Unlink Coder account/, }); - await user.click(ejectButton); - expect(ejectToken).toHaveBeenCalled(); + await user.click(unlinkButton); + expect(unlinkToken).toHaveBeenCalled(); }); }); diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx index 63b9fdd0..efc23329 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthForm/UnlinkAccountButton.tsx @@ -19,7 +19,7 @@ export function UnlinkAccountButton({ ...delegatedProps }: Props) { const styles = useStyles(); - const { ejectToken } = useInternalCoderAuth(); + const { unlinkToken } = useInternalCoderAuth(); return ( { - ejectToken(); + unlinkToken(); onClick?.(event); }} {...delegatedProps} diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index 33b5bc0a..9b4eb549 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -24,7 +24,7 @@ import { CODER_QUERY_KEY_PREFIX, sharedAuthQueryKey, } from '../../api/queryOptions'; -import { coderClientApiRef } from '../../api/CoderClient'; +import { coderClientWrapperApiRef } from '../../api/CoderClient'; import { CoderLogo } from '../CoderLogo'; import { CoderAuthFormDialog } from '../CoderAuthFormDialog'; @@ -67,7 +67,7 @@ export type CoderAuth = Readonly< AuthState & { isAuthenticated: boolean; registerNewToken: (newToken: string) => void; - ejectToken: () => void; + unlinkToken: () => void; } >; @@ -91,7 +91,7 @@ function useAuthState(): CoderAuth { const [readonlyInitialAuthToken] = useState(authToken); const [isInsideGracePeriod, setIsInsideGracePeriod] = useState(true); - const coderClient = useApi(coderClientApiRef); + const coderClient = useApi(coderClientWrapperApiRef); const queryIsEnabled = authToken !== ''; const authValidityQuery = useQuery({ @@ -149,12 +149,14 @@ function useAuthState(): CoderAuth { // Pseudo-mutex; makes sure that if we get a bunch of errors, only one // revalidation will be processed at a time - let isRevalidatingToken = false; + let isRevalidating = false; const revalidateTokenOnError = async (event: QueryCacheNotifyEvent) => { const queryError = event.query.state.error; + const shouldRevalidate = - !isRevalidatingToken && + isAuthenticated && + !isRevalidating && BackstageHttpError.isInstance(queryError) && queryError.status === 401; @@ -162,9 +164,9 @@ function useAuthState(): CoderAuth { return; } - isRevalidatingToken = true; + isRevalidating = true; await queryClient.refetchQueries({ queryKey: sharedAuthQueryKey }); - isRevalidatingToken = false; + isRevalidating = false; }; const queryCache = queryClient.getQueryCache(); @@ -178,7 +180,7 @@ function useAuthState(): CoderAuth { } }, []); - const ejectToken = useCallback(() => { + const unlinkToken = useCallback(() => { setAuthToken(''); window.localStorage.removeItem(TOKEN_STORAGE_KEY); queryClient.removeQueries({ queryKey: [CODER_QUERY_KEY_PREFIX] }); @@ -188,7 +190,7 @@ function useAuthState(): CoderAuth { ...authState, isAuthenticated, registerNewToken, - ejectToken, + unlinkToken, }; } @@ -275,7 +277,7 @@ export function useInternalCoderAuth(): CoderAuth { /** * Exposes Coder auth state to the rest of the UI. */ -// This hook should only be used by end users trying to use the Coder SDK inside +// This hook should only be used by end users trying to use the Coder API inside // Backstage. The hook is renamed on final export to avoid confusion export function useEndUserCoderAuth(): CoderAuth { const authContextValue = useContext(AuthStateContext); @@ -625,7 +627,7 @@ type AuthFallbackProvider = FC< // Matches each behavior for the fallback auth UI to a specific provider. This // is screwy code, but by doing this, we ensure that if the user chooses not to -// have a dynamic auth fallback UI, their app will have far less tracking logic, +// have dynamic a auth fallback UI, their app will have far less tracking logic, // meaning less performance overhead and fewer re-renders from something the // user isn't even using const fallbackProviders = { diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx index 382917d8..b58af930 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx @@ -27,7 +27,10 @@ import { renderHookAsCoderEntity, } from '../../testHelpers/setup'; import { UrlSync, urlSyncApiRef } from '../../api/UrlSync'; -import { CoderClient, coderClientApiRef } from '../../api/CoderClient'; +import { + CoderClientWrapper, + coderClientWrapperApiRef, +} from '../../api/CoderClient'; describe(`${CoderProvider.name}`, () => { describe('AppConfig', () => { @@ -66,7 +69,7 @@ describe(`${CoderProvider.name}`, () => { apis: { discoveryApi, configApi }, }); - const coderClientApi = new CoderClient({ + const coderClientApi = new CoderClientWrapper({ apis: { urlSync, identityApi }, }); @@ -80,7 +83,7 @@ describe(`${CoderProvider.name}`, () => { [discoveryApiRef, discoveryApi], [urlSyncApiRef, urlSync], - [coderClientApiRef, coderClientApi], + [coderClientWrapperApiRef, coderClientApi], ]} > { }); }; - it('Should let the user eject their auth token', async () => { + it('Should let the user unlink their auth token', async () => { const { result } = renderUseCoderAuth(); act(() => result.current.registerNewToken(mockCoderAuthToken)); @@ -109,7 +112,7 @@ describe(`${CoderProvider.name}`, () => { ); }); - act(() => result.current.ejectToken()); + act(() => result.current.unlinkToken()); expect(result.current).toEqual( expect.objectContaining>({ diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx index fd562851..079e1f38 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx @@ -45,8 +45,8 @@ const defaultClient = new QueryClient({ export const CoderProvider = ({ children, appConfig, - queryClient = defaultClient, fallbackAuthUiMode = 'restrained', + queryClient = defaultClient, }: CoderProviderProps) => { return ( diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.test.tsx index 008d931a..d170db36 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.test.tsx @@ -29,8 +29,11 @@ type RenderInputs = Readonly<{ }>; async function renderButton({ buttonText }: RenderInputs) { - const ejectToken = jest.fn(); - const auth: CoderAuth = { ...mockAuthStates.authenticated, ejectToken }; + const unlinkToken = jest.fn(); + const auth: CoderAuth = { + ...mockAuthStates.authenticated, + unlinkToken: unlinkToken, + }; /** * Pretty sure there has to be a more elegant and fault-tolerant way of @@ -58,7 +61,7 @@ async function renderButton({ buttonText }: RenderInputs) { return { ...renderOutput, button: screen.getByRole('button', { name: new RegExp(buttonText) }), - unlinkCoderAccount: ejectToken, + unlinkCoderAccount: unlinkToken, refreshWorkspaces: refetch, }; } diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx index 3d9dbcf6..a6ccfb19 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx @@ -102,7 +102,7 @@ export const ExtraActionsButton = ({ const hookId = useId(); const [loadedAnchor, setLoadedAnchor] = useState(); const refreshWorkspaces = useRefreshWorkspaces(); - const { ejectToken } = useInternalCoderAuth(); + const { unlinkToken } = useInternalCoderAuth(); const styles = useStyles(); const closeMenu = () => setLoadedAnchor(undefined); @@ -178,7 +178,7 @@ export const ExtraActionsButton = ({ { - ejectToken(); + unlinkToken(); closeMenu(); }} > diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts index 5f82e6b7..305a5bab 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { workspaces, workspacesByRepo } from '../../api/queryOptions'; import type { CoderWorkspacesConfig } from '../../hooks/useCoderWorkspacesConfig'; -import { useCoderSdk } from '../../hooks/useCoderSdk'; +import { useCoderApi } from '../../hooks/useCoderApi'; import { useInternalCoderAuth } from '../../components/CoderProvider'; type QueryInput = Readonly<{ @@ -13,13 +13,13 @@ export function useCoderWorkspacesQuery({ coderQuery, workspacesConfig, }: QueryInput) { - const sdk = useCoderSdk(); + const api = useCoderApi(); const auth = useInternalCoderAuth(); const hasRepoData = workspacesConfig && workspacesConfig.repoUrl; const queryOptions = hasRepoData - ? workspacesByRepo({ auth, sdk, coderQuery, workspacesConfig }) - : workspaces({ auth, sdk, coderQuery }); + ? workspacesByRepo({ auth, api, coderQuery, workspacesConfig }) + : workspaces({ auth, api, coderQuery }); return useQuery(queryOptions); } diff --git a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx index 83309a08..65029704 100644 --- a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx +++ b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx @@ -25,7 +25,7 @@ import { getMockQueryClient, } from '../testHelpers/setup'; import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; -import { CODER_QUERY_KEY_PREFIX } from '../api/queryOptions'; +import { CODER_QUERY_KEY_PREFIX } from '../plugin'; import { mockWorkspacesList } from '../testHelpers/mockCoderPluginData'; type RenderUseQueryOptions< @@ -52,11 +52,11 @@ async function renderCoderQuery< } = options; let latestRegisterNewToken!: CoderAuth['registerNewToken']; - let latestEjectToken!: CoderAuth['ejectToken']; + let latestUnlinkToken!: CoderAuth['unlinkToken']; const AuthEscapeHatch = () => { const auth = useEndUserCoderAuth(); latestRegisterNewToken = auth.registerNewToken; - latestEjectToken = auth.ejectToken; + latestUnlinkToken = auth.unlinkToken; return null; }; @@ -91,15 +91,15 @@ async function renderCoderQuery< return act(() => latestRegisterNewToken(mockCoderAuthToken)); }; - const ejectToken = () => { - return act(() => latestEjectToken()); + const unlinkToken = () => { + return act(() => latestUnlinkToken()); }; if (authenticateOnMount) { registerMockToken(); } - return { ...renderOutput, registerMockToken, ejectToken }; + return { ...renderOutput, registerMockToken, unlinkToken }; } describe(`${useCoderQuery.name}`, () => { @@ -113,14 +113,17 @@ describe(`${useCoderQuery.name}`, () => { */ describe('Hook functionality', () => { it('Disables requests while user is not authenticated', async () => { - const { result, registerMockToken, ejectToken } = await renderCoderQuery({ - authenticateOnMount: false, - queryOptions: { - queryKey: ['workspaces'], - queryFn: ({ sdk }) => sdk.getWorkspaces({ q: 'owner:me' }), - select: response => response.workspaces, + const { result, registerMockToken, unlinkToken } = await renderCoderQuery( + { + authenticateOnMount: false, + queryOptions: { + queryKey: ['workspaces'], + queryFn: ({ coderApi: api }) => + api.getWorkspaces({ q: 'owner:me' }), + select: response => response.workspaces, + }, }, - }); + ); expect(result.current.isLoading).toBe(true); registerMockToken(); @@ -131,7 +134,7 @@ describe(`${useCoderQuery.name}`, () => { expect(result.current.data?.length).toBeGreaterThan(0); }); - ejectToken(); + unlinkToken(); await waitFor(() => expect(result.current.isLoading).toBe(true)); }); @@ -181,7 +184,7 @@ describe(`${useCoderQuery.name}`, () => { }); it('Disables everything when the user unlinks their access token', async () => { - const { result, ejectToken } = await renderCoderQuery({ + const { result, unlinkToken } = await renderCoderQuery({ queryOptions: { queryKey: ['workspaces'], queryFn: () => Promise.resolve(mockWorkspacesList), @@ -198,7 +201,7 @@ describe(`${useCoderQuery.name}`, () => { ); }); - ejectToken(); + unlinkToken(); await waitFor(() => { expect(result.current).toEqual( @@ -226,7 +229,7 @@ describe(`${useCoderQuery.name}`, () => { const { promise, reject } = createInvertedPromise(); const queryFn = jest.fn(() => promise); - const { ejectToken } = await renderCoderQuery({ + const { unlinkToken } = await renderCoderQuery({ queryOptions: { queryFn, queryKey: ['blah'], @@ -238,7 +241,7 @@ describe(`${useCoderQuery.name}`, () => { }); await waitFor(() => expect(queryFn).toHaveBeenCalled()); - ejectToken(); + unlinkToken(); queryFn.mockRestore(); act(() => reject(new Error("Don't feel like giving you data today"))); diff --git a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts index 6dff0240..95dcdffd 100644 --- a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts +++ b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts @@ -1,6 +1,6 @@ /** * @file Defines a couple of wrappers over React Query/Tanstack Query that make - * it easier to use the Coder SDK within UI logic. + * it easier to use the Coder API within UI logic. * * These hooks are designed 100% for end-users, and should not be used * internally. Use useEndUserCoderAuth when working with auth logic within these @@ -25,12 +25,12 @@ import { import { DEFAULT_TANSTACK_QUERY_RETRY_COUNT } from '../typesConstants'; import { useEndUserCoderAuth } from '../components/CoderProvider'; import { CODER_QUERY_KEY_PREFIX } from '../api/queryOptions'; -import { useCoderSdk } from './useCoderSdk'; -import type { BackstageCoderSdk } from '../api/CoderClient'; +import { useCoderApi } from './useCoderApi'; +import type { BackstageCoderApi } from '../api/CoderClient'; export type CoderQueryFunctionContext = QueryFunctionContext & { - sdk: BackstageCoderSdk; + coderApi: BackstageCoderApi; }; export type CoderQueryFunction< @@ -63,7 +63,7 @@ export function useCoderQuery< ): UseQueryResult { const queryClient = useQueryClient(); const { isAuthenticated } = useEndUserCoderAuth(); - const sdk = useCoderSdk(); + const coderApi = useCoderApi(); let patchedQueryKey = queryOptions.queryKey; if ( @@ -98,7 +98,7 @@ export function useCoderQuery< throw new Error('Cannot complete request - user is not authenticated'); } - return queryOptions.queryFn({ ...context, sdk }); + return queryOptions.queryFn({ ...context, coderApi }); }, refetchInterval: (data, query) => { diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderApi.ts similarity index 51% rename from plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts rename to plugins/backstage-plugin-coder/src/hooks/useCoderApi.ts index 7b7017a1..962f009c 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderApi.ts @@ -1,13 +1,16 @@ /** - * @file This defines the general helper for accessing the Coder SDK from + * @file This defines the general helper for accessing the Coder API from * Backstage in a type-safe way. * * This hook is meant to be used both internally AND externally. */ import { useApi } from '@backstage/core-plugin-api'; -import { coderClientApiRef, type BackstageCoderSdk } from '../api/CoderClient'; +import { + type BackstageCoderApi, + coderClientWrapperApiRef, +} from '../api/CoderClient'; -export function useCoderSdk(): BackstageCoderSdk { - const { sdk } = useApi(coderClientApiRef); - return sdk; +export function useCoderApi(): BackstageCoderApi { + const { api } = useApi(coderClientWrapperApiRef); + return api; } diff --git a/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx b/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx index 90cac33d..2662b1e6 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx +++ b/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx @@ -8,11 +8,11 @@ import { mockBackstageAssetsEndpoint, mockBackstageUrlRoot, getMockConfigApi, - mockBackstageApiEndpointWithoutSdkPath, + mockBackstageApiEndpointWithoutVersionSuffix, } from '../testHelpers/mockBackstageData'; function renderUseUrlSync() { - let proxyEndpoint: string = mockBackstageApiEndpointWithoutSdkPath; + let proxyEndpoint: string = mockBackstageApiEndpointWithoutVersionSuffix; const mockDiscoveryApi: DiscoveryApi = { getBaseUrl: async () => proxyEndpoint, }; @@ -53,7 +53,7 @@ describe(`${useUrlSync.name}`, () => { state: { baseUrl: mockBackstageUrlRoot, assetsRoute: mockBackstageAssetsEndpoint, - apiRoute: mockBackstageApiEndpointWithoutSdkPath, + apiRoute: mockBackstageApiEndpointWithoutVersionSuffix, }, }), ); diff --git a/plugins/backstage-plugin-coder/src/plugin.ts b/plugins/backstage-plugin-coder/src/plugin.ts index 904b7705..d165c36f 100644 --- a/plugins/backstage-plugin-coder/src/plugin.ts +++ b/plugins/backstage-plugin-coder/src/plugin.ts @@ -8,7 +8,10 @@ import { } from '@backstage/core-plugin-api'; import { rootRouteRef } from './routes'; import { UrlSync, urlSyncApiRef } from './api/UrlSync'; -import { CoderClient, coderClientApiRef } from './api/CoderClient'; +import { + CoderClientWrapper, + coderClientWrapperApiRef, +} from './api/CoderClient'; export const coderPlugin = createPlugin({ id: 'coder', @@ -27,13 +30,13 @@ export const coderPlugin = createPlugin({ }, }), createApiFactory({ - api: coderClientApiRef, + api: coderClientWrapperApiRef, deps: { urlSync: urlSyncApiRef, identityApi: identityApiRef, }, factory: ({ urlSync, identityApi }) => { - return new CoderClient({ + return new CoderClientWrapper({ apis: { urlSync, identityApi }, }); }, @@ -190,10 +193,13 @@ export { useWorkspacesCardContext } from './components/CoderWorkspacesCard/Root' * General custom hooks that can be used in various places. */ export { useCoderWorkspacesConfig } from './hooks/useCoderWorkspacesConfig'; -export { useCoderSdk } from './hooks/useCoderSdk'; -export { useEndUserCoderAuth as useCoderAuth } from './components/CoderProvider/CoderAuthProvider'; +export { useCoderApi } from './hooks/useCoderApi'; export { useCoderQuery } from './hooks/reactQueryWrappers'; +// Deliberately renamed so that end users don't have to be aware that there are +// two different versions of the auth hook +export { useEndUserCoderAuth as useCoderAuth } from './components/CoderProvider/CoderAuthProvider'; + /** * General constants */ @@ -203,3 +209,4 @@ export { CODER_QUERY_KEY_PREFIX } from './api/queryOptions'; * All custom types */ export type { CoderAppConfig } from './components/CoderProvider'; +export type * from './api/vendoredSdk/api/typesGenerated'; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts index 8c96f8d2..843e4743 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts @@ -33,7 +33,10 @@ import { defaultUrlPrefixes, urlSyncApiRef, } from '../api/UrlSync'; -import { CoderClient, coderClientApiRef } from '../api/CoderClient'; +import { + CoderClientWrapper, + coderClientWrapperApiRef, +} from '../api/CoderClient'; /** * This is the key that Backstage checks from the entity data to determine the @@ -68,24 +71,24 @@ export const mockBackstageUrlRoot = 'http://localhost:7007'; /** * A version of the mock API endpoint that doesn't have the Coder API versioning - * prefix. Mainly used for tests that need to assert that the core API URL is - * formatted correctly, before the CoderSdk adds anything else to the end + * suffix. Mainly used for tests that need to assert that the core API URL is + * formatted correctly, before the Coder API adds anything else to the end * * The string literal expression is complicated, but hover over it to see what * the final result is. */ -export const mockBackstageApiEndpointWithoutSdkPath = +export const mockBackstageApiEndpointWithoutVersionSuffix = `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}` as const; /** * The API endpoint to use with the mock server during testing. Adds additional - * path information that will normally be added via the Coder SDK. + * path information that will normally be added via the Coder API. * * The string literal expression is complicated, but hover over it to see what * the final result is. */ export const mockBackstageApiEndpoint = - `${mockBackstageApiEndpointWithoutSdkPath}/api/v2` as const; + `${mockBackstageApiEndpointWithoutVersionSuffix}/api/v2` as const; /** * The assets endpoint to use during testing. @@ -173,7 +176,7 @@ const authedState = { error: undefined, isAuthenticated: true, registerNewToken: jest.fn(), - ejectToken: jest.fn(), + unlinkToken: jest.fn(), } as const satisfies Partial; const notAuthedState = { @@ -181,7 +184,7 @@ const notAuthedState = { error: undefined, isAuthenticated: false, registerNewToken: jest.fn(), - ejectToken: jest.fn(), + unlinkToken: jest.fn(), } as const satisfies Partial; export const mockAuthStates = { @@ -309,7 +312,7 @@ export function getMockApiList(): readonly ApiTuple[] { }, }); - const mockCoderClient = new CoderClient({ + const mockCoderClient = new CoderClientWrapper({ initialToken: mockCoderAuthToken, apis: { urlSync: mockUrlSyncApi, @@ -327,6 +330,6 @@ export function getMockApiList(): readonly ApiTuple[] { // Custom APIs specific to the Coder plugin [urlSyncApiRef, mockUrlSyncApi], - [coderClientApiRef, mockCoderClient], + [coderClientWrapperApiRef, mockCoderClient], ]; } diff --git a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx index cc8c67ad..b7d3191a 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx +++ b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx @@ -10,7 +10,11 @@ import { /* eslint-enable @backstage/no-undeclared-imports */ import React from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + type QueryClientConfig, + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { type CoderAuth, @@ -93,13 +97,16 @@ export function suppressErrorBoundaryWarnings(): void { afterEachCleanupFunctions.push(() => augmentedConsoleError.mockClear()); } -export function getMockQueryClient(): QueryClient { +export function getMockQueryClient(config?: QueryClientConfig): QueryClient { return new QueryClient({ + ...(config ?? {}), defaultOptions: { + ...(config?.defaultOptions ?? {}), queries: { retry: false, refetchOnWindowFocus: false, networkMode: 'offlineFirst', + ...(config?.defaultOptions?.queries ?? {}), }, }, }); From 136c71d3a0b7dfca7e7156ae62b588df3186946d Mon Sep 17 00:00:00 2001 From: BioErrorLog <51422347+bioerrorlog@users.noreply.github.com> Date: Thu, 11 Jul 2024 07:47:23 +0900 Subject: [PATCH 6/9] docs: fix broken api reference link (#136) --- plugins/backstage-plugin-coder/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/backstage-plugin-coder/README.md b/plugins/backstage-plugin-coder/README.md index 657521e2..ba7c6f02 100644 --- a/plugins/backstage-plugin-coder/README.md +++ b/plugins/backstage-plugin-coder/README.md @@ -142,7 +142,7 @@ spec: region: 'us-pittsburgh' ``` -You can find more information about what properties are available (and how they're applied) in our [`catalog-info.yaml` file documentation](./docs/catalog-info.md). +You can find more information about what properties are available (and how they're applied) in our [`catalog-info.yaml` file documentation](./docs/api-reference/catalog-info.md). ## Roadmap From ee842ec8ed7509dc0e29e6b967828c1e6b48dc17 Mon Sep 17 00:00:00 2001 From: BioErrorLog <51422347+bioerrorlog@users.noreply.github.com> Date: Thu, 11 Jul 2024 07:47:42 +0900 Subject: [PATCH 7/9] docs: fix incorrect descriptions in backstage-plugin-coder (#137) * docs: fix subtitle for catalog-info description * docs: fix incorrect reference --- plugins/backstage-plugin-coder/README.md | 2 +- .../backstage-plugin-coder/docs/api-reference/catalog-info.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/backstage-plugin-coder/README.md b/plugins/backstage-plugin-coder/README.md index ba7c6f02..5ccc64a5 100644 --- a/plugins/backstage-plugin-coder/README.md +++ b/plugins/backstage-plugin-coder/README.md @@ -117,7 +117,7 @@ the Dev Container. ); ``` -### `app-config.yaml` files +### `catalog-info.yaml` files In addition to the above, you can define additional properties on your specific repo's `catalog-info.yaml` file. diff --git a/plugins/backstage-plugin-coder/docs/api-reference/catalog-info.md b/plugins/backstage-plugin-coder/docs/api-reference/catalog-info.md index 34fd72b3..cb3d9b56 100644 --- a/plugins/backstage-plugin-coder/docs/api-reference/catalog-info.md +++ b/plugins/backstage-plugin-coder/docs/api-reference/catalog-info.md @@ -42,7 +42,7 @@ This defines the name of the Coder template you would like to use when creating **Note:** This value has overlap with the `defaultTemplateName` property defined in [`CoderAppConfig`](types.md#coderappconfig). In the event that both values are present, the YAML file's `templateName` property will always be used instead. -### `templateName` +### `mode` **Type:** Optional union of `manual` or `auto` From c59f3ec8d12c0d3a3dabc5b83fee67bdfe54ece4 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 18 Jul 2024 09:41:05 -0400 Subject: [PATCH 8/9] add .coder.yaml file (#138) --- .coder.yaml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .coder.yaml diff --git a/.coder.yaml b/.coder.yaml new file mode 100644 index 00000000..e334deda --- /dev/null +++ b/.coder.yaml @@ -0,0 +1,38 @@ +# .coder.yaml +# This is a Coder configuration file. It tells Coder how to create a workspace +# for this repository. You can use variables like {{org}}, {{repo}}, and {{ref}} +# to dynamically generate values. +# +# This configuration works well with Coder's git-clone module. To use it, you +# can add the following to your template: +# +# data "coder_parameter" "git_url" { +# type = "string" +# name = "Git URL" +# description = "The git repository URL to be cloned." +# default = "" +# mutable = true +# } +# +# module "git-clone" { +# source = "registry.coder.com/modules/git-clone/coder" +# version = "1.0.12" +# agent_id = +# url = data.coder_parameter.git_url.value +# } + +# Replace with your Coder deployment URL +host: dev.coder.com + +# Specify the Coder template for this repository +template: dogfood + +# Define a name for the new workspace using variables such as {{org}}, {{repo}}, +# and {{ref}} to dynamically generate values. This name is crucial as it is used +# to identify and potentially reuse an existing workspace within Coder. +name: {{repo}}-{{ref}} + +# Uncomment and use 'parameters' to override template defaults +# parameters: +# - name: "Git URL" +# value: "https://github.com/{{org}}/{{repo}}/tree/{{ref}}" From 9da52e6b2726c65205e6a23a6cd9821865cdad21 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 18 Jul 2024 09:46:49 -0400 Subject: [PATCH 9/9] fix: update base template name for .coder.yaml file (#139) --- .coder.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coder.yaml b/.coder.yaml index e334deda..abdc589b 100644 --- a/.coder.yaml +++ b/.coder.yaml @@ -25,7 +25,7 @@ host: dev.coder.com # Specify the Coder template for this repository -template: dogfood +template: coder # Define a name for the new workspace using variables such as {{org}}, {{repo}}, # and {{ref}} to dynamically generate values. This name is crucial as it is used