8000 feat(Coder plugin): expose Coder SDK to Backstage end-users (#132) · coder/backstage-plugins@251214e · GitHub
[go: up one dir, main page]

Skip to content

Commit 251214e

Browse files
authored
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
1 parent c245950 commit 251214e

File tree

12 files changed

+522
-26
lines changed

12 files changed

+522
-26
lines changed

plugins/backstage-plugin-coder/src/api/queryOptions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig';
44
import type { BackstageCoderSdk } from './CoderClient';
55
import type { CoderAuth } from '../components/CoderProvider';
66

7-
export const CODER_QUERY_KEY_PREFIX = 'coder-backstage-plugin';
7+
// Making the type more broad to hide some implementation details from the end
8+
// user; the prefix should be treated as an opaque string we can change whenever
9+
// we want
10+
export const CODER_QUERY_KEY_PREFIX = 'coder-backstage-plugin' as string;
811

912
// Defined here and not in CoderAuthProvider.ts to avoid circular dependency
1013
// issues

plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ type RestartWorkspaceParameters = Readonly<{
312312

313313
export type DeleteWorkspaceOptions = Pick<
314314
TypesGen.CreateWorkspaceBuildRequest,
315-
'log_level' & 'orphan'
315+
'log_level' | 'orphan'
316316
>;
317317

318318
type Claims = {

plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, {
2+
type FC,
23
type PropsWithChildren,
34
createContext,
45
useCallback,
@@ -136,10 +137,16 @@ function useAuthState(): CoderAuth {
136137
return () => window.clearTimeout(distrustTimeoutId);
137138
}, [authState.status]);
138139

140+
const isAuthenticated = validAuthStatuses.includes(authState.status);
141+
139142
// Sets up subscription to spy on potentially-expired tokens. Can't do this
140143
// outside React because we let the user connect their own queryClient
141144
const queryClient = useQueryClient();
142145
useEffect(() => {
146+
if (!isAuthenticated) {
147+
return undefined;
148+
}
149+
143150
// Pseudo-mutex; makes sure that if we get a bunch of errors, only one
144151
// revalidation will be processed at a time
145152
let isRevalidatingToken = false;
@@ -163,7 +170,7 @@ function useAuthState(): CoderAuth {
163170
const queryCache = queryClient.getQueryCache();
164171
const unsubscribe = queryCache.subscribe(revalidateTokenOnError);
165172
return unsubscribe;
166-
}, [queryClient]);
173+
}, [queryClient, isAuthenticated]);
167174

168175
const registerNewToken = useCallback((newToken: string) => {
169176
if (newToken !== '') {
@@ -179,7 +186,7 @@ function useAuthState(): CoderAuth {
179186

180187
return {
181188
...authState,
182-
isAuthenticated: validAuthStatuses.includes(authState.status),
189+
isAuthenticated,
183190
registerNewToken,
184191
ejectToken,
185192
};
@@ -607,24 +614,75 @@ export const dummyTrackComponent: TrackComponent = () => {
607614
};
608615
};
609616

617+
export type FallbackAuthInputBehavior = 'restrained' | 'assertive' | 'hidden';
618+
type AuthFallbackProvider = FC<
619+
Readonly<
620+
PropsWithChildren<{
621+
isAuthenticated: boolean;
622+
}>
623+
>
624+
>;
625+
626+
// Matches each behavior for the fallback auth UI to a specific provider. This
627+
// is screwy code, but by doing this, we ensure that if the user chooses not to
628+
// have a dynamic auth fallback UI, their app will have far less tracking logic,
629+
// meaning less performance overhead and fewer re-renders from something the
630+
// user isn't even using
631+
const fallbackProviders = {
632+
hidden: ({ children }) => (
633+
<AuthTrackingContext.Provider value={dummyTrackComponent}>
634+
{children}
635+
</AuthTrackingContext.Provider>
636+
),
637+
638+
assertive: ({ children, isAuthenticated }) => (
639+
// Don't need the live version of the tracker function if we're always
640+
// going to be showing the fallback auth input no matter what
641+
<AuthTrackingContext.Provider value={dummyTrackComponent}>
642+
{children}
643+
{!isAuthenticated && <FallbackAuthUi />}
644+
</AuthTrackingContext.Provider>
645+
),
646+
647+
// Have to give function a name to satisfy ES Lint (rules of hooks)
648+
restrained: function Restrained({ children, isAuthenticated }) {
649+
const { hasNoAuthInputs, trackComponent } = useAuthFallbackState();
650+
const needFallbackUi = !isAuthenticated && hasNoAuthInputs;
651+
652+
return (
653+
<>
654+
<AuthTrackingContext.Provider value={trackComponent}>
655+
{children}
656+
</AuthTrackingContext.Provider>
657+
658+
{needFallbackUi && (
659+
<AuthTrackingContext.Provider value={dummyTrackComponent}>
660+
<FallbackAuthUi />
661+
</AuthTrackingContext.Provider>
662+
)}
663+
</>
664+
);
665+
},
666+
} as const satisfies Record<FallbackAuthInputBehavior, AuthFallbackProvider>;
667+
668+
export type CoderAuthProviderProps = Readonly<
669+
PropsWithChildren<{
670+
fallbackAuthUiMode?: FallbackAuthInputBehavior;
671+
}>
672+
>;
673+
610674
export function CoderAuthProvider({
611675
children,
612-
}: Readonly<PropsWithChildren<unknown>>) {
676+
fallbackAuthUiMode = & 10000 #39;restrained',
677+
}: CoderAuthProviderProps) {
613678
const authState = useAuthState();
614-
const { hasNoAuthInputs, trackComponent } = useAuthFallbackState();
615-
const needFallbackUi = !authState.isAuthenticated && hasNoAuthInputs;
679+
const AuthFallbackProvider = fallbackProviders[fallbackAuthUiMode];
616680

617681
return (
618682
<AuthStateContext.Provider value={authState}>
619-
<AuthTrackingContext.Provider value={trackComponent}>
683+
<AuthFallbackProvider isAuthenticated={authState.isAuthenticated}>
620684
{children}
621-
</AuthTrackingContext.Provider>
622-
623-
{needFallbackUi && (
624-
<AuthTrackingContext.Provider value={dummyTrackComponent}>
625-
<FallbackAuthUi />
626-
</AuthTrackingContext.Provider>
627-
)}
685+
</AuthFallbackProvider>
628686
</AuthStateContext.Provider>
629687
);
630688
}

plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ describe(`${CoderProvider.name}`, () => {
8686
<CoderProvider
8787
appConfig={mockAppConfig}
8888
queryClient={getMockQueryClient()}
89+
fallbackAuthUiMode="restrained"
8990
>
9091
{children}
9192
</CoderProvider>

plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,15 @@ export const CoderProvider = ({
4646
children,
4747
appConfig,
4848
queryClient = defaultClient,
49+
fallbackAuthUiMode = 'restrained',
4950
}: CoderProviderProps) => {
5051
return (
5152
<CoderErrorBoundary>
5253
<QueryClientProvider client={queryClient}>
5354
<CoderAppConfigProvider appConfig={appConfig}>
54-
<CoderAuthProvider>{children}</CoderAuthProvider>
55+
<CoderAuthProvider fallbackAuthUiMode={fallbackAuthUiMode}>
56+
{children}
57+
</CoderAuthProvider>
5558
</CoderAppConfigProvider>
5659
</QueryClientProvider>
5760
</CoderErrorBoundary>

plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
type CoderWorkspacesConfig,
1717
} from '../../hooks/useCoderWorkspacesConfig';
1818
import type { Workspace } from '../../api/vendoredSdk';
19-
import { useCoderWorkspacesQuery } from '../../hooks/useCoderWorkspacesQuery';
19+
import { useCoderWorkspacesQuery } from './useCoderWorkspacesQuery';
2020
import { CoderAuthFormCardWrapper } from '../CoderAuthFormCardWrapper';
2121

2222
export type WorkspacesQuery = UseQueryResult<readonly Workspace[]>;

plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts renamed to plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { waitFor } from '@testing-library/react';
22
import { useCoderWorkspacesQuery } from './useCoderWorkspacesQuery';
3-
4-
import { renderHookAsCoderEntity } from '../testHelpers/setup';
5-
import { mockCoderWorkspacesConfig } from '../testHelpers/mockBackstageData';
3+
import { renderHookAsCoderEntity } from '../../testHelpers/setup';
4+
import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData';
65
import {
76
mockWorkspaceNoParameters,
87
mockWorkspacesList,
9-
} from '../testHelpers/mockCoderPluginData';
8+
} from '../../testHelpers/mockCoderPluginData';
109

1110
beforeAll(() => {
1211
jest.useFakeTimers();

plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts renamed to plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { useQuery } from '@tanstack/react-query';
2-
import { workspaces, workspacesByRepo } from '../api/queryOptions';
3-
import type { CoderWorkspacesConfig } from './useCoderWorkspacesConfig';
4-
import { useCoderSdk } from './useCoderSdk';
5-
import { useInternalCoderAuth } from '../components/CoderProvider';
2+
import { workspaces, workspacesByRepo } from '../../api/queryOptions';
3+
import type { CoderWorkspacesConfig } from '../../hooks/useCoderWorkspacesConfig';
4+
import { useCoderSdk } from '../../hooks/useCoderSdk';
5+
import { useInternalCoderAuth } from '../../components/CoderProvider';
66

77
type QueryInput = Readonly<{
88
coderQuery: string;

0 commit comments

Comments
 (0)
0