frontend-app-api: refactor to add CoreRouter extension

Co-authored-by: Camila Belo <camilaibs@gmail.com>
Co-authored-by: Vincenzo Scamporlino <vincenzos@spotify.com>
Co-authored-by: Philipp Hugenroth <philipph@spotify.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-11-15 15:04:11 +01:00
parent c79d9b6e78
commit 07afe68561
6 changed files with 196 additions and 148 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ const signInPageComponentDataRef =
const signInPage = createExtension({
id: 'signInPage',
attachTo: { id: 'core', input: 'signInPage' },
attachTo: { id: 'core.router', input: 'signInPage' },
output: {
component: signInPageComponentDataRef,
},
@@ -14,24 +14,11 @@
* limitations under the License.
*/
import React, { ComponentType, ReactNode, useContext, useState } from 'react';
import {
coreExtensionData,
createExtension,
createExtensionDataRef,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import {
ConfigApi,
IdentityApi,
SignInPageProps,
configApiRef,
useApi,
} from '@backstage/core-plugin-api';
import { InternalAppContext } from '../wiring/InternalAppContext';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
import { BrowserRouter } from 'react-router-dom';
export const Core = createExtension({
id: 'core',
@@ -43,15 +30,6 @@ export const Core = createExtension({
themes: createExtensionInput({
theme: coreExtensionData.theme,
}),
signInPage: createExtensionInput(
{
component:
createExtensionDataRef<ComponentType<SignInPageProps>>(
'core.signInPage',
),
},
{ singleton: true, optional: true },
),
root: createExtensionInput(
{
element: coreExtensionData.reactElement,
@@ -64,120 +42,7 @@ export const Core = createExtension({
},
factory({ inputs }) {
return {
root: (
<AppRouter SignInPageComponent={inputs.signInPage?.component}>
{inputs.root.element}
</AppRouter>
),
root: inputs.root.element,
};
},
});
/**
* Read the configured base path.
*
* The returned path does not have a trailing slash.
*/
function getBasePath(configApi: ConfigApi) {
let { pathname } = new URL(
configApi.getOptionalString('app.baseUrl') ?? '/',
'http://sample.dev', // baseUrl can be specified as just a path
);
pathname = pathname.replace(/\/*$/, '');
return pathname;
}
// This wraps the sign-in page and waits for sign-in to be completed before rendering the app
function SignInPageWrapper({
component: Component,
appIdentityProxy,
children,
}: {
component: ComponentType<SignInPageProps>;
appIdentityProxy: AppIdentityProxy;
children: ReactNode;
}) {
const [identityApi, setIdentityApi] = useState<IdentityApi>();
const configApi = useApi(configApiRef);
const basePath = getBasePath(configApi);
if (!identityApi) {
return <Component onSignInSuccess={setIdentityApi} />;
}
appIdentityProxy.setTarget(identityApi, {
signOutTargetUrl: basePath || '/',
});
return <>{children}</>;
}
/**
* Props for the {@link AppRouter} component.
* @public
*/
export interface AppRouterProps {
children?: ReactNode;
SignInPageComponent?: ComponentType<SignInPageProps>;
}
/**
* App router and sign-in page wrapper.
*
* @public
* @remarks
*
* The AppRouter provides the routing context and renders the sign-in page.
* Until the user has successfully signed in, this component will render
* the sign-in page. Once the user has signed-in, it will instead render
* the app, while providing routing and route tracking for the app.
*/
export function AppRouter(props: AppRouterProps) {
const { children, SignInPageComponent } = props;
const configApi = useApi(configApiRef);
const basePath = getBasePath(configApi);
const internalAppContext = useContext(InternalAppContext);
if (!internalAppContext) {
throw new Error('AppRouter must be rendered within the AppProvider');
}
const { appIdentityProxy } = internalAppContext;
// If the app hasn't configured a sign-in page, we just continue as guest.
if (!SignInPageComponent) {
appIdentityProxy.setTarget(
{
getUserId: () => 'guest',
getIdToken: async () => undefined,
getProfile: () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getProfileInfo: async () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
getCredentials: async () => ({}),
signOut: async () => {},
},
{ signOutTargetUrl: basePath || '/' },
);
return <BrowserRouter basename={basePath}>{children}</BrowserRouter>;
}
return (
<BrowserRouter basename={basePath}>
<SignInPageWrapper
component={SignInPageComponent}
appIdentityProxy={appIdentityProxy}
>
{children}
</SignInPageWrapper>
</BrowserRouter>
);
}
@@ -24,7 +24,7 @@ import { SidebarPage } from '@backstage/core-components';
export const CoreLayout = createExtension({
id: 'core.layout',
attachTo: { id: 'core', input: 'root' },
attachTo: { id: 'core.router', input: 'children' },
inputs: {
nav: createExtensionInput(
{
@@ -0,0 +1,177 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { ComponentType, ReactNode, useContext, useState } from 'react';
import {
coreExtensionData,
createExtension,
createExtensionDataRef,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import {
ConfigApi,
IdentityApi,
SignInPageProps,
configApiRef,
useApi,
} from '@backstage/core-plugin-api';
import { InternalAppContext } from '../wiring/InternalAppContext';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
import { BrowserRouter } from 'react-router-dom';
export const CoreRouter = createExtension({
id: 'core.router',
attachTo: { id: 'core', input: 'root' },
inputs: {
signInPage: createExtensionInput(
{
component:
createExtensionDataRef<ComponentType<SignInPageProps>>(
'core.signInPage',
),
},
{ singleton: true, optional: true },
),
children: createExtensionInput(
{
element: coreExtensionData.reactElement,
},
{ singleton: true },
),
},
output: {
element: coreExtensionData.reactElement,
},
factory({ inputs }) {
return {
element: (
<AppRouter SignInPageComponent={inputs.signInPage?.component}>
{inputs.children.element}
</AppRouter>
),
};
},
});
/**
* Read the configured base path.
*
* The returned path does not have a trailing slash.
*/
function getBasePath(configApi: ConfigApi) {
let { pathname } = new URL(
configApi.getOptionalString('app.baseUrl') ?? '/',
'http://sample.dev', // baseUrl can be specified as just a path
);
pathname = pathname.replace(/\/*$/, '');
return pathname;
}
// This wraps the sign-in page and waits for sign-in to be completed before rendering the app
function SignInPageWrapper({
component: Component,
appIdentityProxy,
children,
}: {
component: ComponentType<SignInPageProps>;
appIdentityProxy: AppIdentityProxy;
children: ReactNode;
}) {
const [identityApi, setIdentityApi] = useState<IdentityApi>();
const configApi = useApi(configApiRef);
const basePath = getBasePath(configApi);
if (!identityApi) {
return <Component onSignInSuccess={setIdentityApi} />;
}
appIdentityProxy.setTarget(identityApi, {
signOutTargetUrl: basePath || '/',
});
return <>{children}</>;
}
/**
* Props for the {@link AppRouter} component.
* @public
*/
export interface AppRouterProps {
children?: ReactNode;
SignInPageComponent?: ComponentType<SignInPageProps>;
}
/**
* App router and sign-in page wrapper.
*
* @public
* @remarks
*
* The AppRouter provides the routing context and renders the sign-in page.
* Until the user has successfully signed in, this component will render
* the sign-in page. Once the user has signed-in, it will instead render
* the app, while providing routing and route tracking for the app.
*/
export function AppRouter(props: AppRouterProps) {
const { children, SignInPageComponent } = props;
const configApi = useApi(configApiRef);
const basePath = getBasePath(configApi);
const internalAppContext = useContext(InternalAppContext);
if (!internalAppContext) {
throw new Error('AppRouter must be rendered within the AppProvider');
}
const { appIdentityProxy } = internalAppContext;
// If the app hasn't configured a sign-in page, we just continue as guest.
if (!SignInPageComponent) {
appIdentityProxy.setTarget(
{
getUserId: () => 'guest',
getIdToken: async () => undefined,
getProfile: () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getProfileInfo: async () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
getCredentials: async () => ({}),
signOut: async () => {},
},
{ signOutTargetUrl: basePath || '/' },
);
return <BrowserRouter basename={basePath}>{children}</BrowserRouter>;
}
return (
<BrowserRouter basename={basePath}>
<SignInPageWrapper
component={SignInPageComponent}
appIdentityProxy={appIdentityProxy}
>
{children}
</SignInPageWrapper>
</BrowserRouter>
);
}
@@ -127,18 +127,22 @@ describe('createApp', () => {
expect(String(tree.root)).toMatchInlineSnapshot(`
"<core out=[core.reactElement]>
root [
<core.layout out=[core.reactElement]>
content [
<core.routes out=[core.reactElement]>
routes [
<plugin.my-plugin.page out=[core.routing.path, core.routing.ref, core.reactElement] />
<core.router out=[core.reactElement]>
children [
<core.layout out=[core.reactElement]>
content [
<core.routes out=[core.reactElement]>
routes [
<plugin.my-plugin.page out=[core.routing.path, core.routing.ref, core.reactElement] />
]
</core.routes>
]
</core.routes>
nav [
<core.nav out=[core.reactElement] />
]
</core.layout>
]
nav [
<core.nav out=[core.reactElement] />
]
</core.layout>
</core.router>
]
themes [
<themes.light out=[core.theme] />
@@ -92,9 +92,11 @@ import { createAppTree } from '../tree';
import { AppNode } from '@backstage/frontend-plugin-api';
import { toLegacyPlugin } from '../routing/toLegacyPlugin';
import { InternalAppContext } from './InternalAppContext';
import { CoreRouter } from '../extensions/CoreRouter';
const builtinExtensions = [
Core,
CoreRouter,
CoreRoutes,
CoreNav,
CoreLayout,