Skip to content

feat(Coder plugin): expose Coder SDK to Backstage end-users #132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a27e95e
chore: add vendored version of experimental Coder SDK
Parkreiner May 24, 2024
9c958d3
chore: update CoderClient class to use new SDK
Parkreiner May 24, 2024
4979067
chore: delete mock SDK
Parkreiner May 24, 2024
5e7e01f
fix: improve data hiding for CoderSdk
Parkreiner May 24, 2024
937f6f5
docs: update typo
Parkreiner May 24, 2024
7e84d00
Merge branch 'mes/vendored-sdk' into mes/vendored-sdk-integration
Parkreiner May 24, 2024
294572d
wip: commit progress on updating Coder client
Parkreiner May 24, 2024
d9626a0
wip: commit more progress on updating types
Parkreiner May 24, 2024
1dcc13b
chore: remove valibot type definitions from global constants file
Parkreiner May 24, 2024
692a763
chore: rename mocks file
Parkreiner May 24, 2024
28accc8
fix: update type mismatches
Parkreiner May 24, 2024
d032768
wip: commit more update progress
Parkreiner May 24, 2024
a76db16
wip: commit progress on updating client/SDK integration
Parkreiner May 24, 2024
d22bc20
fix: get all tests passing for CoderClient
Parkreiner May 24, 2024
08cd049
fix: update UrlSync updates
Parkreiner May 24, 2024
2eb4987
fix: get all tests passing
Parkreiner May 24, 2024
37645f4
chore: update all mock data to use Coder core entity mocks
Parkreiner May 24, 2024
864357d
fix: add extra helpers to useCoderSdk
Parkreiner May 28, 2024
977b2eb
fix: add additional properties to hide from SDK
Parkreiner May 31, 2024
a9b24aa
Merge branch 'mes/vendored-sdk' into mes/vendored-sdk-integration
Parkreiner May 31, 2024
259702e
Merge branch 'main' into mes/vendored-sdk-integration
Parkreiner May 31, 2024
09240cc
fix: shrink down the API of useCoderSdk
Parkreiner May 31, 2024
3a8accb
update method name for clarity
Parkreiner Jun 3, 2024
a67fbcf
chore: removal vestigal endpoint properties
Parkreiner Jun 3, 2024
c3c847a
Merge branch 'main' into mes/vendored-sdk-helpers
Parkreiner Jun 3, 2024
6ef87b7
fix: update reversion
Parkreiner Jun 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion plugins/backstage-plugin-coder/src/api/queryOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ type RestartWorkspaceParameters = Readonly<{

export type DeleteWorkspaceOptions = Pick<
TypesGen.CreateWorkspaceBuildRequest,
'log_level' & 'orphan'
'log_level' | 'orphan'
>;

type Claims = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {
type FC,
type PropsWithChildren,
createContext,
useCallback,
Expand Down Expand Up @@ -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;
Expand All @@ -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 !== '') {
Expand All @@ -179,7 +186,7 @@ function useAuthState(): CoderAuth {

return {
...authState,
isAuthenticated: validAuthStatuses.includes(authState.status),
isAuthenticated,
registerNewToken,
ejectToken,
};
Expand Down Expand Up @@ -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 }) => (
<AuthTrackingContext.Provider value={dummyTrackComponent}>
{children}
</AuthTrackingContext.Provider>
),

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
<AuthTrackingContext.Provider value={dummyTrackComponent}>
{children}
{!isAuthenticated && <FallbackAuthUi />}
</AuthTrackingContext.Provider>
),

// 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 (
<>
<AuthTrackingContext.Provider value={trackComponent}>
{children}
</AuthTrackingContext.Provider>

{needFallbackUi && (
<AuthTrackingContext.Provider value={dummyTrackComponent}>
<FallbackAuthUi />
</AuthTrackingContext.Provider>
)}
</>
);
},
} as const satisfies Record<FallbackAuthInputBehavior, AuthFallbackProvider>;

export type CoderAuthProviderProps = Readonly<
PropsWithChildren<{
fallbackAuthUiMode?: FallbackAuthInputBehavior;
}>
>;

export function CoderAuthProvider({
children,
}: Readonly<PropsWithChildren<unknown>>) {
fallbackAuthUiMode = 'restrained',
}: CoderAuthProviderProps) {
const authState = useAuthState();
const { hasNoAuthInputs, trackComponent } = useAuthFallbackState();
const needFallbackUi = !authState.isAuthenticated && hasNoAuthInputs;
const AuthFallbackProvider = fallbackProviders[fallbackAuthUiMode];

return (
<AuthStateContext.Provider value={authState}>
<AuthTrackingContext.Provider value={trackComponent}>
<AuthFallbackProvider isAuthenticated={authState.isAuthenticated}>
{children}
</AuthTrackingContext.Provider>

{needFallbackUi && (
<AuthTrackingContext.Provider value={dummyTrackComponent}>
<FallbackAuthUi />
</AuthTrackingContext.Provider>
)}
</AuthFallbackProvider>
</AuthStateContext.Provider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ describe(`${CoderProvider.name}`, () => {
<CoderProvider
appConfig={mockAppConfig}
queryClient={getMockQueryClient()}
fallbackAuthUiMode="restrained"
>
{children}
</CoderProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ export const CoderProvider = ({
children,
appConfig,
queryClient = defaultClient,
fallbackAuthUiMode = 'restrained',
}: CoderProviderProps) => {
return (
<CoderErrorBoundary>
<QueryClientProvider client={queryClient}>
<CoderAppConfigProvider appConfig={appConfig}>
<CoderAuthProvider>{children}</CoderAuthProvider>
<CoderAuthProvider fallbackAuthUiMode={fallbackAuthUiMode}>
{children}
</CoderAuthProvider>
</CoderAppConfigProvider>
</QueryClientProvider>
</CoderErrorBoundary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<readonly Workspace[]>;
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading