Merge pull request #19769 from backstage/mob/routing
frontend-app-api: initial WIP routing implementation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { graphiqlPlugin as legacyGraphiqlPlugin } from '@backstage/plugin-graphiql';
|
||||
import { createApp as createLegacyApp } from '@backstage/app-defaults';
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { pagesPlugin } from './examples/pagesPlugin';
|
||||
import { graphiqlPlugin } from './examples/graphiqlPlugin';
|
||||
|
||||
/*
|
||||
@@ -49,7 +50,7 @@ TODO:
|
||||
/* app.tsx */
|
||||
|
||||
const app = createApp({
|
||||
plugins: [graphiqlPlugin],
|
||||
plugins: [graphiqlPlugin, pagesPlugin],
|
||||
// bindRoutes({ bind }) {
|
||||
// bind(catalogPlugin.externalRoutes, {
|
||||
// createComponent: scaffolderPlugin.routes.root,
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 { Link } from '@backstage/core-components';
|
||||
import {
|
||||
createPageExtension,
|
||||
createPlugin,
|
||||
useRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createRouteRef } 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: '/',
|
||||
routeRef: indexRouteRef,
|
||||
component: async () => {
|
||||
const Component = () => {
|
||||
const page1Link = useRouteRef(page1RouteRef);
|
||||
return (
|
||||
<div>
|
||||
op
|
||||
<div>
|
||||
<Link to={page1Link()}>Page 1</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link to="/graphiql">GraphiQL</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return <Component />;
|
||||
},
|
||||
});
|
||||
|
||||
const Page1 = createPageExtension({
|
||||
id: 'page1',
|
||||
defaultPath: '/page1',
|
||||
routeRef: page1RouteRef,
|
||||
component: async () => {
|
||||
const Component = () => {
|
||||
const indexLink = useRouteRef(indexRouteRef);
|
||||
// const page2Link = useRouteRef(page2RouteRef);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>This is page 1</h1>
|
||||
<Link to={indexLink()}>Go back</Link>
|
||||
<Link to="./page2">Page 2</Link>
|
||||
{/* <Link to={page2Link()}>Page 2</Link> */}
|
||||
|
||||
<div>
|
||||
Sub-page content:
|
||||
<div>
|
||||
<Routes>
|
||||
<Route path="/" element={<h2>This is also page 1</h2>} />
|
||||
<Route path="/page2" element={<h2>This is page 2</h2>} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return <Component />;
|
||||
},
|
||||
});
|
||||
|
||||
export const pagesPlugin = createPlugin({
|
||||
id: 'pages',
|
||||
// routes: {
|
||||
// index: indexRouteRef,
|
||||
// // reference in config:
|
||||
// // 'plugin.pages.routes.index'
|
||||
// // OR
|
||||
// // 'page1'
|
||||
// },
|
||||
extensions: [IndexPage, Page1],
|
||||
});
|
||||
@@ -33,6 +33,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-graphiql": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
BackstagePlugin,
|
||||
coreExtensionData,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { RouteExtension } from './extensions/RouteExtension';
|
||||
import { CoreRouter } from './extensions/CoreRouter';
|
||||
import {
|
||||
createExtensionInstance,
|
||||
ExtensionInstance,
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
mergeExtensionParameters,
|
||||
readAppExtensionParameters,
|
||||
} from './wiring/parameters';
|
||||
import { RoutingProvider } from './routing/RoutingContext';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
/** @public */
|
||||
export function createApp(options: { plugins: BackstagePlugin[] }): {
|
||||
@@ -37,7 +39,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
|
||||
} {
|
||||
const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any);
|
||||
|
||||
const builtinExtensions = [RouteExtension];
|
||||
const builtinExtensions = [CoreRouter];
|
||||
|
||||
// pull in default extension instance from discovered packages
|
||||
// apply config to adjust default extension instances and add more
|
||||
@@ -83,7 +85,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
|
||||
return existingInstance;
|
||||
}
|
||||
|
||||
const attachments = Object.fromEntries(
|
||||
const attachments = new Map(
|
||||
Array.from(
|
||||
attachmentMap.get(instanceParams.extension.id)?.entries() ?? [],
|
||||
).map(([inputName, attachmentConfigs]) => [
|
||||
@@ -92,12 +94,16 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
|
||||
]),
|
||||
);
|
||||
|
||||
return createExtensionInstance({
|
||||
const newInstance = createExtensionInstance({
|
||||
extension: instanceParams.extension,
|
||||
source: instanceParams.source,
|
||||
config: instanceParams.config,
|
||||
attachments,
|
||||
});
|
||||
|
||||
instances.set(instanceParams.extension.id, newInstance);
|
||||
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
const rootConfigs = attachmentMap.get('root')?.get('default') ?? [];
|
||||
@@ -105,6 +111,8 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
|
||||
createInstance(instanceParams),
|
||||
);
|
||||
|
||||
const routePaths = extractRouteInfoFromInstanceTree(rootInstances);
|
||||
|
||||
return {
|
||||
createRoot() {
|
||||
const rootComponents = rootInstances.map(
|
||||
@@ -114,12 +122,49 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
|
||||
) as typeof coreExtensionData.reactComponent.T,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<RoutingProvider routePaths={routePaths}>
|
||||
{rootComponents.map((Component, i) => (
|
||||
<Component key={i} />
|
||||
))}
|
||||
</>
|
||||
</RoutingProvider>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function extractRouteInfoFromInstanceTree(
|
||||
roots: ExtensionInstance[],
|
||||
): Map<RouteRef, string> {
|
||||
const results = new Map<RouteRef, string>();
|
||||
|
||||
function visit(current: ExtensionInstance, basePath: string) {
|
||||
const routePath = current.data.get(coreExtensionData.routePath.id) ?? '';
|
||||
const routeRef = current.data.get(
|
||||
coreExtensionData.routeRef.id,
|
||||
) as RouteRef;
|
||||
|
||||
// TODO: join paths in a more robust way
|
||||
const fullPath = basePath + routePath;
|
||||
if (routeRef) {
|
||||
const routeRefId = (routeRef as any).id; // TODO: properly
|
||||
if (routeRefId !== current.id) {
|
||||
throw new Error(
|
||||
`Route ref '${routeRefId}' must have the same ID as extension '${current.id}'`,
|
||||
);
|
||||
}
|
||||
results.set(routeRef, fullPath);
|
||||
}
|
||||
|
||||
for (const children of current.attachments.values()) {
|
||||
for (const child of children) {
|
||||
visit(child, fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of roots) {
|
||||
visit(root, '');
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -19,9 +19,16 @@ import mapValues from 'lodash/mapValues';
|
||||
|
||||
/** @internal */
|
||||
export interface ExtensionInstance {
|
||||
id: string;
|
||||
data: Map<string, unknown>;
|
||||
$$type: 'extension-instance';
|
||||
readonly id: string;
|
||||
/**
|
||||
* Maps extension data ref IDs to extensions produced.
|
||||
*/
|
||||
readonly data: Map<string, unknown>;
|
||||
/**
|
||||
* Maps input names to the actual instances given to them.
|
||||
*/
|
||||
readonly attachments: Map<string, ExtensionInstance[]>;
|
||||
readonly $$type: 'extension-instance';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -29,7 +36,7 @@ export function createExtensionInstance(options: {
|
||||
extension: Extension<unknown>;
|
||||
config: unknown;
|
||||
source?: BackstagePlugin;
|
||||
attachments: Record<string, ExtensionInstance[]>;
|
||||
attachments: Map<string, ExtensionInstance[]>;
|
||||
}): ExtensionInstance {
|
||||
const { extension, config, source, attachments } = options;
|
||||
const extensionData = new Map<string, unknown>();
|
||||
@@ -54,7 +61,7 @@ export function createExtensionInstance(options: {
|
||||
extension.inputs,
|
||||
({ extensionData: pointData }, inputName) => {
|
||||
// TODO: validation
|
||||
return (attachments[inputName] ?? []).map(attachment =>
|
||||
return (attachments.get(inputName) ?? []).map(attachment =>
|
||||
mapValues(pointData, ref => attachment.data.get(ref.id)),
|
||||
);
|
||||
},
|
||||
@@ -69,6 +76,7 @@ export function createExtensionInstance(options: {
|
||||
return {
|
||||
id: options.extension.id,
|
||||
data: extensionData,
|
||||
attachments,
|
||||
$$type: 'extension-instance',
|
||||
};
|
||||
}
|
||||
|
||||
+2
-1
@@ -21,13 +21,14 @@ import {
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { BrowserRouter, useRoutes } from 'react-router-dom';
|
||||
|
||||
export const RouteExtension = createExtension({
|
||||
export const CoreRouter = createExtension({
|
||||
id: 'core.router',
|
||||
at: 'root',
|
||||
inputs: {
|
||||
routes: {
|
||||
extensionData: {
|
||||
path: coreExtensionData.routePath,
|
||||
ref: coreExtensionData.routeRef,
|
||||
component: coreExtensionData.reactComponent,
|
||||
},
|
||||
},
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 React, { createContext, ReactNode } from 'react';
|
||||
|
||||
export interface RoutingContextType {
|
||||
resolve(
|
||||
routeRef: RouteRef,
|
||||
options: { pathname: string },
|
||||
): (() => string) | undefined;
|
||||
}
|
||||
|
||||
export const RoutingContext = createContext<RoutingContextType>({
|
||||
resolve: () => () => '',
|
||||
});
|
||||
|
||||
export class RouteResolver {
|
||||
constructor(private readonly routePaths: Map<RouteRef, string>) {}
|
||||
|
||||
resolve(anyRouteRef: RouteRef<{}>): (() => string) | undefined {
|
||||
const basePath = this.routePaths.get(anyRouteRef);
|
||||
if (!basePath) {
|
||||
return undefined;
|
||||
}
|
||||
return () => basePath;
|
||||
}
|
||||
}
|
||||
|
||||
export function RoutingProvider(props: {
|
||||
routePaths: Map<RouteRef, string>;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<RoutingContext.Provider value={new RouteResolver(props.routePaths)}>
|
||||
{props.children}
|
||||
</RoutingContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { ComponentType } from 'react';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { z } from 'zod';
|
||||
import { ZodSchema } from 'zod';
|
||||
import { ZodTypeDef } from 'zod';
|
||||
@@ -33,6 +34,7 @@ export const coreExtensionData: {
|
||||
reactComponent: ExtensionDataRef<ComponentType<{}>>;
|
||||
routePath: ExtensionDataRef<string>;
|
||||
apiFactory: ExtensionDataRef<AnyApiFactory>;
|
||||
routeRef: ExtensionDataRef<RouteRef<any>>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -138,6 +140,7 @@ export function createPageExtension<
|
||||
at?: string;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
routeRef?: RouteRef;
|
||||
component: (props: {
|
||||
config: TConfig;
|
||||
inputs: {
|
||||
@@ -230,4 +233,7 @@ export type PortableSchema<TOutput> = {
|
||||
parse: (input: unknown) => TOutput;
|
||||
schema: JsonObject;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export function useRouteRef(routeRef: RouteRef<any>): () => string;
|
||||
```
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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<
|
||||
</React.Suspense>
|
||||
</ExtensionBoundary>
|
||||
));
|
||||
if (options.routeRef) {
|
||||
bind.routeRef!(options.routeRef);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,3 +37,4 @@ export {
|
||||
type ExtensionDataValue,
|
||||
} from './types';
|
||||
export * from './wiring';
|
||||
export * from './routing';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { useRouteRef } from './useRouteRef';
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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';
|
||||
// eslint-disable-next-line @backstage/no-forbidden-package-imports
|
||||
import { RoutingContext } from '@backstage/frontend-app-api/src/routing/RoutingContext';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
/** @public */
|
||||
export function useRouteRef(routeRef: RouteRef<any>): () => string {
|
||||
const { pathname } = useLocation();
|
||||
const resolver = useContext(RoutingContext);
|
||||
|
||||
const routeFunc = useMemo(
|
||||
() => resolver && resolver.resolve(routeRef, { pathname }),
|
||||
[resolver, routeRef, pathname],
|
||||
);
|
||||
|
||||
if (!routeFunc) {
|
||||
throw new Error(`Failed to resolve routeRef ${routeRef}`);
|
||||
}
|
||||
|
||||
return routeFunc;
|
||||
}
|
||||
@@ -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<ComponentType>('core.reactComponent'),
|
||||
routePath: createExtensionDataRef<string>('core.routing.path'),
|
||||
apiFactory: createExtensionDataRef<AnyApiFactory>('core.api.factory'),
|
||||
routeRef: createExtensionDataRef<RouteRef>('core.routing.ref'),
|
||||
};
|
||||
|
||||
/** @public */
|
||||
|
||||
Reference in New Issue
Block a user