From 07afe68561fe14b279858199415457d8363ae6da Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 15:04:11 +0100 Subject: [PATCH] frontend-app-api: refactor to add CoreRouter extension Co-authored-by: Camila Belo Co-authored-by: Vincenzo Scamporlino Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/app-next/src/App.tsx | 2 +- .../frontend-app-api/src/extensions/Core.tsx | 137 +------------- .../src/extensions/CoreLayout.tsx | 2 +- .../src/extensions/CoreRouter.tsx | 177 ++++++++++++++++++ .../src/wiring/createApp.test.tsx | 24 ++- .../frontend-app-api/src/wiring/createApp.tsx | 2 + 6 files changed, 196 insertions(+), 148 deletions(-) create mode 100644 packages/frontend-app-api/src/extensions/CoreRouter.tsx diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 236616867a..43b93cb14b 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -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, }, diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx index 12ea9756f1..60772d435c 100644 --- a/packages/frontend-app-api/src/extensions/Core.tsx +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -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>( - 'core.signInPage', - ), - }, - { singleton: true, optional: true }, - ), root: createExtensionInput( { element: coreExtensionData.reactElement, @@ -64,120 +42,7 @@ export const Core = createExtension({ }, factory({ inputs }) { return { - root: ( - - {inputs.root.element} - - ), + 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; - appIdentityProxy: AppIdentityProxy; - children: ReactNode; -}) { - const [identityApi, setIdentityApi] = useState(); - const configApi = useApi(configApiRef); - const basePath = getBasePath(configApi); - - if (!identityApi) { - return ; - } - - appIdentityProxy.setTarget(identityApi, { - signOutTargetUrl: basePath || '/', - }); - return <>{children}; -} - -/** - * Props for the {@link AppRouter} component. - * @public - */ -export interface AppRouterProps { - children?: ReactNode; - SignInPageComponent?: ComponentType; -} - -/** - * 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 {children}; - } - - return ( - - - {children} - - - ); -} diff --git a/packages/frontend-app-api/src/extensions/CoreLayout.tsx b/packages/frontend-app-api/src/extensions/CoreLayout.tsx index 97f49acac9..0099b67751 100644 --- a/packages/frontend-app-api/src/extensions/CoreLayout.tsx +++ b/packages/frontend-app-api/src/extensions/CoreLayout.tsx @@ -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( { diff --git a/packages/frontend-app-api/src/extensions/CoreRouter.tsx b/packages/frontend-app-api/src/extensions/CoreRouter.tsx new file mode 100644 index 0000000000..712b3d065c --- /dev/null +++ b/packages/frontend-app-api/src/extensions/CoreRouter.tsx @@ -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>( + 'core.signInPage', + ), + }, + { singleton: true, optional: true }, + ), + children: createExtensionInput( + { + element: coreExtensionData.reactElement, + }, + { singleton: true }, + ), + }, + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }) { + return { + element: ( + + {inputs.children.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; + appIdentityProxy: AppIdentityProxy; + children: ReactNode; +}) { + const [identityApi, setIdentityApi] = useState(); + const configApi = useApi(configApiRef); + const basePath = getBasePath(configApi); + + if (!identityApi) { + return ; + } + + appIdentityProxy.setTarget(identityApi, { + signOutTargetUrl: basePath || '/', + }); + return <>{children}; +} + +/** + * Props for the {@link AppRouter} component. + * @public + */ +export interface AppRouterProps { + children?: ReactNode; + SignInPageComponent?: ComponentType; +} + +/** + * 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 {children}; + } + + return ( + + + {children} + + + ); +} diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index 80d9f846f0..52186a5029 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -127,18 +127,22 @@ describe('createApp', () => { expect(String(tree.root)).toMatchInlineSnapshot(` " root [ - - content [ - - routes [ - + + children [ + + content [ + + routes [ + + ] + ] - + nav [ + + ] + ] - nav [ - - ] - + ] themes [ diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 59e6fb6786..1858bd4884 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -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,