frontend-app-api: route sign-in errors through app boundary
Rethrow sign-in bootstrap failures from inside the prepared sign-in tree so the app root extension boundary handles them instead of createApp keeping its own error state. This keeps bootstrap error handling aligned with the rest of the extension tree. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -187,6 +187,86 @@ describe('createApp', () => {
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should surface sign-in bootstrap errors through the app root boundary', async () => {
|
||||
const identityApi = {
|
||||
getProfileInfo: async () => ({ displayName: 'Test User' }),
|
||||
getBackstageIdentity: async () => ({
|
||||
type: 'user' as const,
|
||||
userEntityRef: 'user:default/test-user',
|
||||
ownershipEntityRefs: ['user:default/test-user'],
|
||||
}),
|
||||
getCredentials: async () => ({ token: 'token' }),
|
||||
signOut: async () => {},
|
||||
};
|
||||
const featureFlagsApi = {
|
||||
isActive: jest.fn(() => {
|
||||
throw new Error('sign-in bootstrap failed');
|
||||
}),
|
||||
registerFlag: jest.fn(),
|
||||
getRegisteredFlags: () => [],
|
||||
save: jest.fn(),
|
||||
} as unknown as typeof featureFlagsApiRef.T;
|
||||
|
||||
const app = createApp({
|
||||
advanced: {
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
},
|
||||
features: [
|
||||
appPluginOriginal,
|
||||
createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
ApiBlueprint.make({
|
||||
params: defineParams =>
|
||||
defineParams({
|
||||
api: featureFlagsApiRef,
|
||||
deps: {},
|
||||
factory: () => featureFlagsApi,
|
||||
}),
|
||||
}),
|
||||
appPluginOriginal.getExtension('sign-in-page:app').override({
|
||||
factory: () => {
|
||||
function SignInPage(props: {
|
||||
onSignInSuccess(identity: IdentityApi): void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
props.onSignInSuccess(identityApi);
|
||||
}, [props]);
|
||||
|
||||
return <div>Custom Sign In</div>;
|
||||
}
|
||||
|
||||
return [signInPageComponentDataRef(SignInPage)];
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
createFrontendPlugin({
|
||||
pluginId: 'test',
|
||||
featureFlags: [{ name: 'test-flag' }],
|
||||
extensions: [
|
||||
PageBlueprint.make({
|
||||
if: { featureFlags: { $contains: 'test-flag' } },
|
||||
params: {
|
||||
path: '/',
|
||||
loader: async () => <div>Flagged Page</div>,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await renderWithEffects(app.createRoot());
|
||||
|
||||
await expect(
|
||||
screen.findByText(/Error in app/),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText('sign-in bootstrap failed'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should deduplicate features keeping the last received one', async () => {
|
||||
const duplicatedFeatureId = 'test';
|
||||
const app = createApp({
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { JSX, lazy, ReactNode, Suspense, useReducer, useState } from 'react';
|
||||
import { JSX, lazy, ReactNode, Suspense, useReducer } from 'react';
|
||||
import {
|
||||
ConfigApi,
|
||||
coreExtensionData,
|
||||
@@ -151,13 +151,8 @@ function PreparedAppRoot(props: {
|
||||
}): JSX.Element {
|
||||
const signIn = props.preparedApp.getSignIn();
|
||||
const SignIn = signIn.Component;
|
||||
const [finalizeError, setFinalizeError] = useState<Error>();
|
||||
const [, triggerRerender] = useReducer((count: number) => count + 1, 0);
|
||||
|
||||
if (finalizeError) {
|
||||
throw finalizeError;
|
||||
}
|
||||
|
||||
const finalizedApp: FinalizedSpecializedApp | undefined =
|
||||
props.preparedApp.tryFinalize();
|
||||
|
||||
@@ -167,7 +162,6 @@ function PreparedAppRoot(props: {
|
||||
onReady={() => {
|
||||
triggerRerender();
|
||||
}}
|
||||
onError={setFinalizeError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
JSX,
|
||||
} from 'react';
|
||||
import {
|
||||
ExtensionBoundary,
|
||||
coreExtensionData,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
@@ -84,7 +85,7 @@ export const AppRoot = createExtension({
|
||||
),
|
||||
},
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory({ inputs, apis }) {
|
||||
factory({ inputs, apis, node }) {
|
||||
if (isProtectedApp()) {
|
||||
const identityApi = apis.get(identityApiRef);
|
||||
if (!identityApi) {
|
||||
@@ -123,19 +124,21 @@ export const AppRoot = createExtension({
|
||||
|
||||
return [
|
||||
coreExtensionData.reactElement(
|
||||
<AppRouter
|
||||
SignInPageComponent={inputs.signInPage?.get(
|
||||
SignInPageBlueprint.dataRefs.component,
|
||||
)}
|
||||
RouterComponent={inputs.router?.get(
|
||||
RouterBlueprint.dataRefs.component,
|
||||
)}
|
||||
extraElements={inputs.elements?.map(el =>
|
||||
el.get(coreExtensionData.reactElement),
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</AppRouter>,
|
||||
<ExtensionBoundary node={node}>
|
||||
<AppRouter
|
||||
SignInPageComponent={inputs.signInPage?.get(
|
||||
SignInPageBlueprint.dataRefs.component,
|
||||
)}
|
||||
RouterComponent={inputs.router?.get(
|
||||
RouterBlueprint.dataRefs.component,
|
||||
)}
|
||||
extraElements={inputs.elements?.map(el =>
|
||||
el.get(coreExtensionData.reactElement),
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</AppRouter>
|
||||
</ExtensionBoundary>,
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user