diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 49a2c9681c..3d29f19c13 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -1,107 +1,146 @@ app: packages: 'all' # ✨ - extensions: - - graphiql.page: - config: - path: / - # - core.signInPage: - # props: - # provider: - # id: google - # title: Google - # message: Sign In using Google - # apiRef: googleAuthApiRef # ??? - # - core.nav: - # config: - # logo: assets/logo.png - # layout: - # - label: Search - # icon: search - # to: /search - # items: - # - point: search - # - type: divider - # - label: Menu - # icon: menu - # items: - # - label: Home - # icon: home - # to: /catalog - # - label: Create... - # icon: create - # to: /create - # - type: divider - # - type: scroll-wrapper - # items: - # - label: Tech Radar - # icon: map - # to: /tech-radar - # - label: GraphiQL - # icon: graphiql - # to: /graphiql - # - type: divider - # - point: shortcuts - # - type: space - # - type: divider - # - label: Settings - # icon: avatar - # to: /settings - # items: - # - point: settings - # - core.nav/search: '@backstage/plugin-search#SidebarSearchModal' - # - core.nav/shortcuts: - # use: '@backstage/plugin-shortcuts#Shortcuts' - # config: - # allowExternalLinks: true - # - core.nav/settings: '@backstage/plugin-user-settings#SidebarSettings' + # scmAuthExtension: >- + # createScmAuthExtension({ + # id: 'apis.scmAuth.addons.ghe', + # factory({ bind }) { + # bind.scmAuthAddon({ + # baseUrl: 'https://github.spotify.net', + # api: githubAuthApiRef, + # }) + # } + # }) - # - core.pages.index: - # at: - # point: core.routes - # config: - # path: / - # use: 'react-router-dom#Navigate' - # config: - # to: /catalog + # externalRouteRefs: + # bind(catalogPlugin.externalRoutes, { + # createComponent: scaffolderPlugin.routes.root, + # viewTechDoc: techdocsPlugin.routes.docRoot, + # createFromTemplate: scaffolderPlugin.routes.selectedTemplate, + # }); + # bind(apiDocsPlugin.externalRoutes, { + # registerApi: catalogImportPlugin.routes.importPage, + # }); + # bind(scaffolderPlugin.externalRoutes, { + # registerComponent: catalogImportPlugin.routes.importPage, + # viewTechDoc: techdocsPlugin.routes.docRoot, + # }); + # bind(orgPlugin.externalRoutes, { + # catalogIndex: catalogPlugin.routes.catalogIndex, + # }); - # - scaffolder.page: - # config: - # groups: - # - title: Recommended - # filter: - # entity.metadata.tags: recommended - # - scaffolder.page/fields: '@backstage/plugin-scaffolder#LowerCaseValuePickerFieldExtension' - # - scaffolder.page/layout: '@backstage/plugin-scaffolder#TwoColumnLayout' + # extensions: + # - plugin.catalog: + # config: + # externalRoutes: + # createComponent: plugin.scaffolder.page + # viewTechDoc: plugin.techdocs.docRootPage + # createFromTemplate: plugin.scaffolder.templatePage + # - graphiql.page: + # config: + # path: / + # - apis.auth.providers.github: + # config: + # provider: ghe - # - search.page/content: 'app#customSearchPage' # custom search page from somewhere + # - core.signInPage: + # props: + # provider: + # id: google + # title: Google + # message: Sign In using Google + # apiRef: googleAuthApiRef # ??? - # - user-settings.page.routes.advanced: - # at: - # point: user-settings.page/routes - # config: - # title: Advanced - # path: /advanced - # use: '@backstage/plugin-user-settings#AdvancedSettings' + # - core.nav: + # config: + # logo: assets/logo.png + # layout: + # - label: Search + # icon: search + # to: /search + # items: + # - point: search + # - type: divider + # - label: Menu + # icon: menu + # items: + # - label: Home + # icon: home + # to: /catalog + # - label: Create... + # icon: create + # to: /create + # - type: divider + # - type: scroll-wrapper + # items: + # - label: Tech Radar + # icon: map + # to: /tech-radar + # - label: GraphiQL + # icon: graphiql + # to: /graphiql + # - type: divider + # - point: shortcuts + # - type: space + # - type: divider + # - label: Settings + # icon: avatar + # to: /settings + # items: + # - point: settings + # - core.nav/search: '@backstage/plugin-search#SidebarSearchModal' + # - core.nav/shortcuts: + # use: '@backstage/plugin-shortcuts#Shortcuts' + # config: + # allowExternalLinks: true + # - core.nav/settings: '@backstage/plugin-user-settings#SidebarSettings' - # - entity.card.orphanWarning - # - entity.card.processingErrors - # - entity.card.about - # - entity.card.catalogGraph - # - entity.card.pagerDuty - # - entity.card.links - # - entity.card.labels - # - entity.card.githubInsightsLanguages - # - entity.card.githubInsightsReleases - # - entity.card.githubInsightsReadme: - # config: - # maxHeight: 350 # Throwing this config in to have an example, but in practice rely on default - # - entity.card.subcomponentsCard - # - entity.card.userProfile - # - entity.card.ownership - # - entity.card.likeDislikeRatings - # - entity.content.dependsOnComponents - # - entity.content.codeInsights - # - entity.content.todo - # - entity.content.feedbackResponse + # - core.pages.index: + # at: + # point: core.routes + # config: + # path: / + # use: 'react-router-dom#Navigate' + # config: + # to: /catalog + + # - scaffolder.page: + # config: + # groups: + # - title: Recommended + # filter: + # entity.metadata.tags: recommended + # - scaffolder.page/fields: '@backstage/plugin-scaffolder#LowerCaseValuePickerFieldExtension' + # - scaffolder.page/layout: '@backstage/plugin-scaffolder#TwoColumnLayout' + + # - search.page/content: 'app#customSearchPage' # custom search page from somewhere + + # - user-settings.page.routes.advanced: + # at: + # point: user-settings.page/routes + # config: + # title: Advanced + # path: /advanced + # use: '@backstage/plugin-user-settings#AdvancedSettings' + + # - entity.card.orphanWarning + # - entity.card.processingErrors + # - entity.card.about + # - entity.card.catalogGraph + # - entity.card.pagerDuty + # - entity.card.links + # - entity.card.labels + # - entity.card.githubInsightsLanguages + # - entity.card.githubInsightsReleases + # - entity.card.githubInsightsReadme: + # config: + # maxHeight: 350 # Throwing this config in to have an example, but in practice rely on default + # - entity.card.subcomponentsCard + # - entity.card.userProfile + # - entity.card.ownership + # - entity.card.likeDislikeRatings + # - entity.content.dependsOnComponents + # - entity.content.codeInsights + # - entity.content.todo + # - entity.content.feedbackResponse diff --git a/packages/app-next/src/examples/graphiqlPlugin.tsx b/packages/app-next/src/examples/graphiqlPlugin.tsx index 4e3768dafa..e80e137ae5 100644 --- a/packages/app-next/src/examples/graphiqlPlugin.tsx +++ b/packages/app-next/src/examples/graphiqlPlugin.tsx @@ -23,6 +23,7 @@ import React from 'react'; export const GraphiqlPage = createPageExtension({ id: 'graphiql.page', defaultPath: '/graphiql', + disabled: true, component: () => import('@backstage/plugin-graphiql').then(({ Router }) => ), }); diff --git a/packages/app-next/src/examples/pagesPlugin.tsx b/packages/app-next/src/examples/pagesPlugin.tsx index 4bb0b76ad7..4551f0c8f7 100644 --- a/packages/app-next/src/examples/pagesPlugin.tsx +++ b/packages/app-next/src/examples/pagesPlugin.tsx @@ -19,19 +19,31 @@ import { Link } from '@backstage/core-components'; import { createPageExtension, createPlugin, + useRouteRef, } from '@backstage/frontend-plugin-api'; +import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api'; +import { Route, Routes } from 'react-router-dom'; + +const indexRouteRef = createRouteRef({ id: 'index' }); +const page1RouteRef = createRouteRef({ id: 'page1' }); +// const page2RouteRef = createSubRouteRef({ +// id: 'page2', +// parent: page1RouteRef, +// path: '/page2', +// }); const IndexPage = createPageExtension({ id: 'index', defaultPath: '/', + // indexRouteRef component: async () => { const Component = () => { - // const page1 = useRouteRef(); + const page1Link = useRouteRef(page1RouteRef); return (
op
- Page 1 + Page 1
GraphiQL @@ -46,17 +58,42 @@ const IndexPage = createPageExtension({ const Page1 = createPageExtension({ id: 'page1', defaultPath: '/page1', + routeRef: page1RouteRef, component: async () => { - const Component = () => ( -
-

This is page 1

-
- ); + const Component = () => { + const indexLink = useRouteRef(indexRouteRef); + // const page2Link = useRouteRef(page2RouteRef); + + return ( +
+

This is page 1

+ Go back + {/* Page 2 */} + +
+ Sub-page content: +
+ + This is also page 1} /> + This is page 2} /> + +
+
+
+ ); + }; return ; }, }); export const pagesPlugin = createPlugin({ id: 'pages', + // routes: { + // index: indexRouteRef, + // // reference in config: + // // 'plugin.pages.routes.index' + // // OR + // // 'page1' + // }, extensions: [IndexPage, Page1], }); diff --git a/packages/frontend-app-api/src/routing/RoutingContext.ts b/packages/frontend-app-api/src/routing/RoutingContext.ts new file mode 100644 index 0000000000..968cb82d5f --- /dev/null +++ b/packages/frontend-app-api/src/routing/RoutingContext.ts @@ -0,0 +1,25 @@ +/* + * 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 { RouteRef } from '@backstage/core-plugin-api'; +import { createContext } from 'react'; + +export interface RoutingContextType { + resolve(routeRef: RouteRef, options: { pathname: string }): () => string; +} + +export const RoutingContext = createContext({ + resolve: () => () => '', +}); diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index fa2b4ff093..ba13135b60 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -32,7 +32,8 @@ "dist" ], "peerDependencies": { - "react": "^16.13.1 || ^17.0.0" + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "dependencies": { "@backstage/core-plugin-api": "workspace:^", diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index b3f8942d0d..fb2d930844 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { RouteRef } from '@backstage/core-plugin-api'; import React from 'react'; import { ExtensionBoundary } from '../components'; import { createSchemaFromZod, PortableSchema } from '../createSchemaFromZod'; @@ -46,6 +47,7 @@ export function createPageExtension< at?: string; disabled?: boolean; inputs?: TInputs; + routeRef?: RouteRef; component: (props: { config: TConfig; inputs: { @@ -70,6 +72,7 @@ export function createPageExtension< output: { component: coreExtensionData.reactComponent, path: coreExtensionData.routePath, + ...(options.routeRef && { routeRef: coreExtensionData.routeRef }), }, inputs: options.inputs, configSchema, @@ -87,6 +90,9 @@ export function createPageExtension< )); + if (options.routeRef) { + bind.routeRef!(options.routeRef); + } }, }); } diff --git a/packages/frontend-plugin-api/src/routing/useRouteRef.ts b/packages/frontend-plugin-api/src/routing/useRouteRef.ts new file mode 100644 index 0000000000..f828240966 --- /dev/null +++ b/packages/frontend-plugin-api/src/routing/useRouteRef.ts @@ -0,0 +1,32 @@ +/* + * 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 { RouteRef } from '@backstage/core-plugin-api'; +import { RoutingContext } from '@backstage/frontend-app-api/src/routing/RoutingContext'; +import { useContext, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; + +export function useRouteRef(routeRef: RouteRef): () => string | undefined { + const { pathname } = useLocation(); + const resolver = useContext(RoutingContext); + + const routeFunc = useMemo( + () => resolver && resolver.resolve(routeRef, { pathname }), + [resolver, routeRef, pathname], + ); + + return routeFunc; +} diff --git a/packages/frontend-plugin-api/src/types.ts b/packages/frontend-plugin-api/src/types.ts index 8c59deabfb..940e821af5 100644 --- a/packages/frontend-plugin-api/src/types.ts +++ b/packages/frontend-plugin-api/src/types.ts @@ -15,6 +15,7 @@ */ import { AnyApiFactory } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { PortableSchema } from './createSchemaFromZod'; import { BackstagePlugin } from './wiring'; @@ -37,6 +38,7 @@ export const coreExtensionData = { reactComponent: createExtensionDataRef('core.reactComponent'), routePath: createExtensionDataRef('core.routing.path'), apiFactory: createExtensionDataRef('core.api.factory'), + routeRef: createExtensionDataRef('core.routing.ref'), }; /** @public */