From c79d9b6e78fdeb7985f523e1d78c4537caef47b9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 14:48:52 +0100 Subject: [PATCH 1/8] frontend-app-api: working sign-in page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Camila Belo Co-authored-by: Fredrik Adelöw Co-authored-by: Vincenzo Scamporlino Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/app-next/src/App.tsx | 58 +++++++- .../frontend-app-api/src/extensions/Core.tsx | 137 +++++++++++++++++- .../src/wiring/InternalAppContext.ts | 26 ++++ .../frontend-app-api/src/wiring/createApp.tsx | 45 ++---- 4 files changed, 230 insertions(+), 36 deletions(-) create mode 100644 packages/frontend-app-api/src/wiring/InternalAppContext.ts diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 978c40ef44..236616867a 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { createApp } from '@backstage/frontend-app-api'; import { pagesPlugin } from './examples/pagesPlugin'; import graphiqlPlugin from '@backstage/plugin-graphiql/alpha'; @@ -29,6 +29,7 @@ import { createExtension, createApiExtension, createExtensionOverrides, + createExtensionDataRef, } from '@backstage/frontend-plugin-api'; import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; import { homePage } from './HomePage'; @@ -36,12 +37,17 @@ import { collectLegacyRoutes } from '@backstage/core-compat-api'; import { FlatRoutes } from '@backstage/core-app-api'; import { Route } from 'react-router'; import { CatalogImportPage } from '@backstage/plugin-catalog-import'; -import { createApiFactory, configApiRef } from '@backstage/core-plugin-api'; +import { + createApiFactory, + configApiRef, + SignInPageProps, +} from '@backstage/core-plugin-api'; import { ScmAuth, ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; +import Button from '@material-ui/core/Button'; /* @@ -84,6 +90,47 @@ const homePageExtension = createExtension({ }, }); +const signInPageComponentDataRef = + createExtensionDataRef>('core.signInPage'); + +const signInPage = createExtension({ + id: 'signInPage', + attachTo: { id: 'core', input: 'signInPage' }, + output: { + component: signInPageComponentDataRef, + }, + factory() { + return { + component: (props: SignInPageProps) => ( +
+

Sign in page

+
+ +
+
+ ), + }; + }, +}); + const scmAuthExtension = createApiExtension({ factory: ScmAuth.createDefaultApiFactory(), }); @@ -112,7 +159,12 @@ const app = createApp({ homePlugin, ...collectedLegacyPlugins, createExtensionOverrides({ - extensions: [homePageExtension, scmAuthExtension, scmIntegrationApi], + extensions: [ + homePageExtension, + scmAuthExtension, + scmIntegrationApi, + signInPage, + ], }), ], /* Handled through config instead */ diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx index 60772d435c..12ea9756f1 100644 --- a/packages/frontend-app-api/src/extensions/Core.tsx +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -14,11 +14,24 @@ * 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', @@ -30,6 +43,15 @@ export const Core = createExtension({ themes: createExtensionInput({ theme: coreExtensionData.theme, }), + signInPage: createExtensionInput( + { + component: + createExtensionDataRef>( + 'core.signInPage', + ), + }, + { singleton: true, optional: true }, + ), root: createExtensionInput( { element: coreExtensionData.reactElement, @@ -42,7 +64,120 @@ 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/wiring/InternalAppContext.ts b/packages/frontend-app-api/src/wiring/InternalAppContext.ts new file mode 100644 index 0000000000..805c46a067 --- /dev/null +++ b/packages/frontend-app-api/src/wiring/InternalAppContext.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2022 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 { createContext } from 'react'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy'; + +export const InternalAppContext = createContext< + | undefined + | { + appIdentityProxy: AppIdentityProxy; + } +>(undefined); diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index d39111eb99..59e6fb6786 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -76,7 +76,7 @@ import { components as defaultComponents, icons as defaultIcons, } from '../../../app-defaults/src/defaults'; -import { BrowserRouter, Route } from 'react-router-dom'; +import { Route } from 'react-router-dom'; import { SidebarItem } from '@backstage/core-components'; import { DarkTheme, LightTheme } from '../extensions/themes'; import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode'; @@ -91,6 +91,7 @@ import { collectRouteIds } from '../routing/collectRouteIds'; import { createAppTree } from '../tree'; import { AppNode } from '@backstage/frontend-plugin-api'; import { toLegacyPlugin } from '../routing/toLegacyPlugin'; +import { InternalAppContext } from './InternalAppContext'; const builtinExtensions = [ Core, @@ -299,7 +300,8 @@ export function createSpecializedApp(options?: { ), ); - const apiHolder = createApiHolder(tree, config); + const appIdentityProxy = new AppIdentityProxy(); + const apiHolder = createApiHolder(tree, config, appIdentityProxy); const routeInfo = extractRouteInfoFromAppNode(tree.root); const routeBindings = resolveRouteBindings( options?.bindRoutes, @@ -313,8 +315,9 @@ export function createSpecializedApp(options?: { - {/* TODO: set base path using the logic from AppRouter */} - {rootEl} + + {rootEl} + @@ -350,7 +353,11 @@ function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext { }; } -function createApiHolder(tree: AppTree, configApi: ConfigApi): ApiHolder { +function createApiHolder( + tree: AppTree, + configApi: ConfigApi, + appIdentityProxy: AppIdentityProxy, +): ApiHolder { const factoryRegistry = new ApiFactoryRegistry(); const pluginApis = @@ -379,33 +386,7 @@ function createApiHolder(tree: AppTree, configApi: ConfigApi): ApiHolder { factoryRegistry.register('static', { api: identityApiRef, deps: {}, - factory: () => { - const appIdentityProxy = new AppIdentityProxy(); - // TODO: Remove this when sign-in page is migrated - 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: '/' }, - ); - return appIdentityProxy; - }, + factory: () => appIdentityProxy, }); factoryRegistry.register('static', { From 07afe68561fe14b279858199415457d8363ae6da Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 15:04:11 +0100 Subject: [PATCH 2/8] 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, From 640f05f485117da5f610a0f7b2d48e1c8632604d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 15:24:58 +0100 Subject: [PATCH 3/8] frontend-plugin-api: add createSignInPageExtension 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 | 66 +++++++--------- .../src/extensions/CoreRouter.tsx | 8 +- packages/frontend-plugin-api/api-report.md | 20 +++++ .../extensions/createSignInPageExtension.tsx | 79 +++++++++++++++++++ .../src/extensions/index.ts | 1 + 5 files changed, 131 insertions(+), 43 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 43b93cb14b..987dac8e29 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType } from 'react'; +import React from 'react'; import { createApp } from '@backstage/frontend-app-api'; import { pagesPlugin } from './examples/pagesPlugin'; import graphiqlPlugin from '@backstage/plugin-graphiql/alpha'; @@ -29,7 +29,6 @@ import { createExtension, createApiExtension, createExtensionOverrides, - createExtensionDataRef, } from '@backstage/frontend-plugin-api'; import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; import { homePage } from './HomePage'; @@ -48,6 +47,7 @@ import { scmIntegrationsApiRef, } from '@backstage/integration-react'; import Button from '@material-ui/core/Button'; +import { createSignInPageExtension } from '@backstage/frontend-plugin-api'; /* @@ -90,45 +90,35 @@ const homePageExtension = createExtension({ }, }); -const signInPageComponentDataRef = - createExtensionDataRef>('core.signInPage'); - -const signInPage = createExtension({ +const signInPage = createSignInPageExtension({ id: 'signInPage', - attachTo: { id: 'core.router', input: 'signInPage' }, - output: { - component: signInPageComponentDataRef, - }, - factory() { - return { - component: (props: SignInPageProps) => ( + loader: async () => (props: SignInPageProps) => + ( +
+

Sign in page

-

Sign in page

-
- -
+
- ), - }; - }, +
+ ), }); const scmAuthExtension = createApiExtension({ diff --git a/packages/frontend-app-api/src/extensions/CoreRouter.tsx b/packages/frontend-app-api/src/extensions/CoreRouter.tsx index 712b3d065c..f128a246e6 100644 --- a/packages/frontend-app-api/src/extensions/CoreRouter.tsx +++ b/packages/frontend-app-api/src/extensions/CoreRouter.tsx @@ -18,7 +18,6 @@ import React, { ComponentType, ReactNode, useContext, useState } from 'react'; import { coreExtensionData, createExtension, - createExtensionDataRef, createExtensionInput, } from '@backstage/frontend-plugin-api'; import { @@ -32,6 +31,8 @@ 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'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { signInPageComponentDataRef } from '../../../frontend-plugin-api/src/extensions/createSignInPageExtension'; export const CoreRouter = createExtension({ id: 'core.router', @@ -39,10 +40,7 @@ export const CoreRouter = createExtension({ inputs: { signInPage: createExtensionInput( { - component: - createExtensionDataRef>( - 'core.signInPage', - ), + component: signInPageComponentDataRef, }, { singleton: true, optional: true }, ), diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index f1537fd5b7..68aa34dff5 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -68,6 +68,7 @@ import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { SessionApi } from '@backstage/core-plugin-api'; import { SessionState } from '@backstage/core-plugin-api'; +import { SignInPageProps } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { storageApiRef } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; @@ -454,6 +455,25 @@ export function createSchemaFromZod( schemaCreator: (zImpl: typeof z) => ZodSchema, ): PortableSchema; +// @public (undocumented) +export function createSignInPageExtension< + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + id: string; + attachTo?: { + id: string; + input: string; + }; + configSchema?: PortableSchema; + disabled?: boolean; + inputs?: TInputs; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise>; +}): Extension; + // @public export function createSubRouteRef< Path extends string, diff --git a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx new file mode 100644 index 0000000000..048be333b7 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx @@ -0,0 +1,79 @@ +/* + * 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, lazy } from 'react'; +import { ExtensionBoundary } from '../components'; +import { PortableSchema } from '../schema'; +import { + createExtension, + Extension, + ExtensionInputValues, + AnyExtensionInputMap, + createExtensionDataRef, +} from '../wiring'; +import { Expand } from '../types'; +import { SignInPageProps } from '@backstage/core-plugin-api'; + +/** @internal */ +export const signInPageComponentDataRef = + createExtensionDataRef>('core.signInPage'); + +/** + * + * @public + */ +export function createSignInPageExtension< + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + id: string; + attachTo?: { id: string; input: string }; + configSchema?: PortableSchema; + disabled?: boolean; + inputs?: TInputs; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise>; +}): Extension { + const { id } = options; + + return createExtension({ + id, + attachTo: options.attachTo ?? { id: 'core.router', input: 'signInPage' }, + configSchema: options.configSchema, + inputs: options.inputs, + disabled: options.disabled, + output: { + component: signInPageComponentDataRef, + }, + factory({ config, inputs, source }) { + const ExtensionComponent = lazy(() => + options + .loader({ config, inputs }) + .then(component => ({ default: component })), + ); + + return { + component: props => ( + + + + ), + }; + }, + }); +} diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index 00cf4f919c..d696774272 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -17,4 +17,5 @@ export { createApiExtension } from './createApiExtension'; export { createPageExtension } from './createPageExtension'; export { createNavItemExtension } from './createNavItemExtension'; +export { createSignInPageExtension } from './createSignInPageExtension'; export { createThemeExtension } from './createThemeExtension'; From 1c956bcdc8b5fea8c570271f3cbee9a19bef5907 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 15:26:48 +0100 Subject: [PATCH 4/8] app-next: switch to existing sign-in page 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 | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 987dac8e29..67faf8d646 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -46,8 +46,8 @@ import { ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; -import Button from '@material-ui/core/Button'; import { createSignInPageExtension } from '@backstage/frontend-plugin-api'; +import { SignInPage } from '@backstage/core-components'; /* @@ -93,32 +93,7 @@ const homePageExtension = createExtension({ const signInPage = createSignInPageExtension({ id: 'signInPage', loader: async () => (props: SignInPageProps) => - ( -
-

Sign in page

-
- -
-
- ), + , }); const scmAuthExtension = createApiExtension({ From 78a0f6d6af74b13c285e83b8a348001413150616 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 15:37:03 +0100 Subject: [PATCH 5/8] frontend-test-utils: update to attach subject to core.router Co-authored-by: Camila Belo Co-authored-by: Vincenzo Scamporlino Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/frontend-test-utils/src/app/createExtensionTester.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.ts b/packages/frontend-test-utils/src/app/createExtensionTester.ts index 34833dfc69..c95ece342d 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.ts +++ b/packages/frontend-test-utils/src/app/createExtensionTester.ts @@ -67,7 +67,7 @@ export class ExtensionTester { })), { [subject.extension.id]: { - attachTo: { id: 'core', input: 'root' }, + attachTo: { id: 'core.router', input: 'children' }, config: subject.config, disabled: false, }, From 68310c4df52b5cda0acff155c7d00f1dce9dd9bd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 15:37:22 +0100 Subject: [PATCH 6/8] frontend-plugin-api: add test for createSignInPageExtension Co-authored-by: Camila Belo Co-authored-by: Vincenzo Scamporlino Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .../createSignInPageExtension.test.tsx | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/createSignInPageExtension.test.tsx diff --git a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.test.tsx new file mode 100644 index 0000000000..964941e213 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.test.tsx @@ -0,0 +1,47 @@ +/* + * 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 from 'react'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { screen } from '@testing-library/react'; +import { createSignInPageExtension } from './createSignInPageExtension'; +import { coreExtensionData, createExtension } from '../wiring'; + +describe('createSignInPageExtension', () => { + it('renders a sign-in page', async () => { + const SignInPage = createSignInPageExtension({ + id: 'test', + loader: async () => () =>
, + }); + + createExtensionTester( + createExtension({ + id: 'dummy', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + element: coreExtensionData.reactElement, + }, + factory: () => ({ element:
}), + }), + ) + .add(SignInPage) + .render(); + + await expect( + screen.findByTestId('sign-in-page'), + ).resolves.toBeInTheDocument(); + }); +}); From 0403c3be64aca53637af8461022a4463879d5bfb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 16 Nov 2023 17:00:55 +0100 Subject: [PATCH 7/8] frontend-*: test fixes for core.router addition Signed-off-by: Patrik Oldsberg --- .../src/routing/extractRouteInfoFromAppNode.test.ts | 3 ++- packages/frontend-plugin-api/src/wiring/createPlugin.test.ts | 4 ++-- .../src/app/createExtensionTester.test.tsx | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index c44c10180a..112e1435ae 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -33,6 +33,7 @@ import { Core } from '../extensions/Core'; import { CoreRoutes } from '../extensions/CoreRoutes'; import { CoreNav } from '../extensions/CoreNav'; import { CoreLayout } from '../extensions/CoreLayout'; +import { CoreRouter } from '../extensions/CoreRouter'; const ref1 = createRouteRef(); const ref2 = createRouteRef(); @@ -79,7 +80,7 @@ function routeInfoFromExtensions(extensions: Extension[]) { }); const tree = createAppTree({ config: new MockConfigApi({}), - builtinExtensions: [Core, CoreRoutes, CoreNav, CoreLayout], + builtinExtensions: [Core, CoreRoutes, CoreNav, CoreLayout, CoreRouter], features: [plugin], }); diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index 42648fcd96..f69b54aa42 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -133,7 +133,7 @@ describe('createPlugin', () => { await renderWithEffects( createTestAppRoot({ features: [plugin], - config: { app: { extensions: [{ 'core.layout': false }] } }, + config: { app: { extensions: [{ 'core.router': false }] } }, }), ); @@ -161,7 +161,7 @@ describe('createPlugin', () => { config: { app: { extensions: [ - { 'core.layout': false }, + { 'core.router': false }, { 'plugin.catalog.page': { config: { name: 'CatalogRenamed' }, diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index 946dd3cb0c..398acf4c74 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -62,7 +62,7 @@ describe('createExtensionTester', () => { }), ).render(), ).toThrow( - "Failed to instantiate extension 'core', input 'root' did not receive required extension data 'core.reactElement' from extension 'test'", + "Failed to instantiate extension 'core.router', input 'children' did not receive required extension data 'core.reactElement' from extension 'test'", ); }); }); From e5397354351c3e95e02be30536c4a3e1d52322ee Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Nov 2023 12:44:40 +0100 Subject: [PATCH 8/8] changeset: added changesets for sign-in page in DI Signed-off-by: Patrik Oldsberg --- .changeset/fifty-seas-grab.md | 5 +++++ .changeset/hip-berries-begin.md | 5 +++++ .changeset/nervous-dancers-wait.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/fifty-seas-grab.md create mode 100644 .changeset/hip-berries-begin.md create mode 100644 .changeset/nervous-dancers-wait.md diff --git a/.changeset/fifty-seas-grab.md b/.changeset/fifty-seas-grab.md new file mode 100644 index 0000000000..587ac5606d --- /dev/null +++ b/.changeset/fifty-seas-grab.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': minor +--- + +Updated core extension structure to make space for the sign-in page by adding `core.router`. diff --git a/.changeset/hip-berries-begin.md b/.changeset/hip-berries-begin.md new file mode 100644 index 0000000000..43d176cbcf --- /dev/null +++ b/.changeset/hip-berries-begin.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Updates for `core.router` addition. diff --git a/.changeset/nervous-dancers-wait.md b/.changeset/nervous-dancers-wait.md new file mode 100644 index 0000000000..388d995487 --- /dev/null +++ b/.changeset/nervous-dancers-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added `createSignInPageExtension`.