Merge pull request #20574 from backstage/rugvip/routerefs
frontend-*-api: reimplement routing system from core-*-api
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': minor
|
||||
---
|
||||
|
||||
Added `RouteRef`, `SubRouteRef`, `ExternalRouteRef`, and related types. All exports from this package that previously relied on the types with the same name from `@backstage/core-plugin-api` now use the new types instead. To convert and existing legacy route ref to be compatible with the APIs from this package, use the `convertLegacyRouteRef` utility from `@backstage/core-plugin-api/alpha`.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/plugin-user-settings': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-tech-radar': patch
|
||||
'@backstage/plugin-graphiql': patch
|
||||
'@backstage/plugin-search': patch
|
||||
---
|
||||
|
||||
Updated alpha exports according to routing changes in `@backstage/frontend-plugin-api`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': minor
|
||||
---
|
||||
|
||||
Added the ability to configure bound routes through `app.routes.bindings`. The routing system used by `createApp` has been replaced by one that only supports route refs of the new format from `@backstage/frontend-plugin-api`. The requirement for route refs to have the same ID as their associated extension has been removed.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-plugin-api': patch
|
||||
---
|
||||
|
||||
Deprecated several types related to the routing system that are scheduled to be removed, as well as several fields on the route ref types themselves.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-plugin-api': minor
|
||||
---
|
||||
|
||||
Introduced `AnyRouteRefParams` as a replacement for `AnyParams`, which is now deprecated.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-plugin-api': patch
|
||||
---
|
||||
|
||||
Added a new `/alpha` export `convertLegacyRouteRef`, which is a temporary utility to allow existing route refs to be used with the new experimental packages.
|
||||
@@ -3,13 +3,11 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnyExternalRoutes } from '@backstage/core-plugin-api';
|
||||
import { AnyRoutes } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { default as React_2 } from 'react';
|
||||
|
||||
// @public (undocumented)
|
||||
const examplePlugin: BackstagePlugin<AnyRoutes, AnyExternalRoutes>;
|
||||
const examplePlugin: BackstagePlugin<{}, {}>;
|
||||
export default examplePlugin;
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
app:
|
||||
experimental:
|
||||
packages: 'all' # ✨
|
||||
routes:
|
||||
bindings:
|
||||
plugin.pages.externalRoutes.pageX: plugin.pages.routes.pageX
|
||||
|
||||
extensions:
|
||||
- apis.plugin.graphiql.browse.gitlab: true
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import techdocsPlugin from '@backstage/plugin-techdocs/alpha';
|
||||
import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/*
|
||||
|
||||
@@ -59,7 +60,7 @@ TODO:
|
||||
const entityPageExtension = createPageExtension({
|
||||
id: 'catalog:entity',
|
||||
defaultPath: '/catalog/:namespace/:kind/:name',
|
||||
routeRef: entityRouteRef,
|
||||
routeRef: convertLegacyRouteRef(entityRouteRef),
|
||||
loader: async () => <div>Just a temporary mocked entity page</div>,
|
||||
});
|
||||
|
||||
@@ -74,9 +75,10 @@ const app = createApp({
|
||||
extensions: [entityPageExtension],
|
||||
}),
|
||||
],
|
||||
bindRoutes({ bind }) {
|
||||
bind(pagesPlugin.externalRoutes, { pageX: pagesPlugin.routes.pageX });
|
||||
},
|
||||
/* Handled through config instead */
|
||||
// bindRoutes({ bind }) {
|
||||
// bind(pagesPlugin.externalRoutes, { pageX: pagesPlugin.routes.pageX });
|
||||
// },
|
||||
});
|
||||
|
||||
// const legacyApp = createLegacyApp({ plugins: [legacyGraphiqlPlugin] });
|
||||
|
||||
@@ -19,18 +19,16 @@ import { Link } from '@backstage/core-components';
|
||||
import {
|
||||
createPageExtension,
|
||||
createPlugin,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
useRouteRef,
|
||||
createRouteRef,
|
||||
createExternalRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
useRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
const indexRouteRef = createRouteRef({ id: 'index' });
|
||||
const page1RouteRef = createRouteRef({ id: 'page1' });
|
||||
export const externalPageXRouteRef = createExternalRouteRef({ id: 'pageX' });
|
||||
export const pageXRouteRef = createRouteRef({ id: 'pageX' });
|
||||
const indexRouteRef = createRouteRef();
|
||||
const page1RouteRef = createRouteRef();
|
||||
export const externalPageXRouteRef = createExternalRouteRef();
|
||||
export const pageXRouteRef = createRouteRef();
|
||||
// const page2RouteRef = createSubRouteRef({
|
||||
// id: 'page2',
|
||||
// parent: page1RouteRef,
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnyRouteRefParams } from '@backstage/core-plugin-api';
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/core-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { SubRouteRef } from '@backstage/core-plugin-api';
|
||||
import { TranslationMessages as TranslationMessages_2 } from '@backstage/core-plugin-api/alpha';
|
||||
import { TranslationRef as TranslationRef_2 } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
@@ -25,6 +29,24 @@ export type AppLanguageApi = {
|
||||
// @alpha (undocumented)
|
||||
export const appLanguageApiRef: ApiRef<AppLanguageApi>;
|
||||
|
||||
// @public
|
||||
export function convertLegacyRouteRef<TParams extends AnyRouteRefParams>(
|
||||
ref: RouteRef<TParams>,
|
||||
): NewRouteRef<TParams>;
|
||||
|
||||
// @public
|
||||
export function convertLegacyRouteRef<TParams extends AnyRouteRefParams>(
|
||||
ref: SubRouteRef<TParams>,
|
||||
): NewSubRouteRef<TParams>;
|
||||
|
||||
// @public
|
||||
export function convertLegacyRouteRef<
|
||||
TParams extends AnyRouteRefParams,
|
||||
TOptional extends boolean,
|
||||
>(
|
||||
ref: ExternalRouteRef<TParams, TOptional>,
|
||||
): NewExternalRouteRef<TParams, TOptional>;
|
||||
|
||||
// @alpha
|
||||
export function createTranslationMessages<
|
||||
TId extends string,
|
||||
|
||||
@@ -95,8 +95,11 @@ export type AnyExternalRoutes = {
|
||||
[name: string]: ExternalRouteRef;
|
||||
};
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type AnyParams = AnyRouteRefParams;
|
||||
|
||||
// @public
|
||||
export type AnyParams =
|
||||
export type AnyRouteRefParams =
|
||||
| {
|
||||
[param in string]: string;
|
||||
}
|
||||
@@ -523,7 +526,7 @@ export type IdentityApi = {
|
||||
// @public
|
||||
export const identityApiRef: ApiRef<IdentityApi>;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type MakeSubRouteRef<
|
||||
Params extends {
|
||||
[param in string]: string;
|
||||
@@ -533,7 +536,7 @@ export type MakeSubRouteRef<
|
||||
? SubRouteRef<OptionalParams<MergeParams<Params, ParentParams>>>
|
||||
: never;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type MergeParams<
|
||||
P1 extends {
|
||||
[param in string]: string;
|
||||
@@ -606,30 +609,30 @@ export type OpenIdConnectApi = {
|
||||
getIdToken(options?: AuthRequestOptions): Promise<string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type OptionalParams<
|
||||
Params extends {
|
||||
[param in string]: string;
|
||||
},
|
||||
> = Params[keyof Params] extends never ? undefined : Params;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type ParamKeys<Params extends AnyParams> = keyof Params extends never
|
||||
? []
|
||||
: (keyof Params)[];
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type ParamNames<S extends string> =
|
||||
S extends `${infer Part}/${infer Rest}`
|
||||
? ParamPart<Part> | ParamNames<Rest>
|
||||
: ParamPart<S>;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type ParamPart<S extends string> = S extends `:${infer Param}`
|
||||
? Param
|
||||
: never;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type PathParams<S extends string> = {
|
||||
[name in ParamNames<S>]: string;
|
||||
};
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export * from './translation';
|
||||
export * from './apis/alpha';
|
||||
export { convertLegacyRouteRef } from './routing/convertLegacyRouteRef';
|
||||
|
||||
@@ -51,6 +51,7 @@ export class SubRouteRefImpl<Params extends AnyParams>
|
||||
/**
|
||||
* Used in {@link PathParams} type declaration.
|
||||
* @public
|
||||
* @deprecated this type is deprecated and will be removed in the future
|
||||
*/
|
||||
export type ParamPart<S extends string> = S extends `:${infer Param}`
|
||||
? Param
|
||||
@@ -59,6 +60,7 @@ export type ParamPart<S extends string> = S extends `:${infer Param}`
|
||||
/**
|
||||
* Used in {@link PathParams} type declaration.
|
||||
* @public
|
||||
* @deprecated this type is deprecated and will be removed in the future
|
||||
*/
|
||||
export type ParamNames<S extends string> =
|
||||
S extends `${infer Part}/${infer Rest}`
|
||||
@@ -68,12 +70,14 @@ export type ParamNames<S extends string> =
|
||||
* This utility type helps us infer a Param object type from a string path
|
||||
* For example, `/foo/:bar/:baz` inferred to `{ bar: string, baz: string }`
|
||||
* @public
|
||||
* @deprecated this type is deprecated and will be removed in the future
|
||||
*/
|
||||
export type PathParams<S extends string> = { [name in ParamNames<S>]: string };
|
||||
|
||||
/**
|
||||
* Merges a param object type with an optional params type into a params object.
|
||||
* @public
|
||||
* @deprecated this type is deprecated and will be removed in the future
|
||||
*/
|
||||
export type MergeParams<
|
||||
P1 extends { [param in string]: string },
|
||||
@@ -85,6 +89,7 @@ export type MergeParams<
|
||||
* The parameters types are merged together while ensuring that there is no overlap between the two.
|
||||
*
|
||||
* @public
|
||||
* @deprecated this type is deprecated and will be removed in the future
|
||||
*/
|
||||
export type MakeSubRouteRef<
|
||||
Params extends { [param in string]: string },
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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 { routeRefType } from './types';
|
||||
import {
|
||||
RouteRef as LegacyRouteRef,
|
||||
SubRouteRef as LegacySubRouteRef,
|
||||
ExternalRouteRef as LegacyExternalRouteRef,
|
||||
AnyRouteRefParams,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
// Relative imports to avoid dependency, at least for now
|
||||
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
createRouteRef,
|
||||
createSubRouteRef,
|
||||
createExternalRouteRef,
|
||||
} from '../../../frontend-plugin-api/src/routing';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalRouteRef } from '../../../frontend-plugin-api/src/routing/RouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalSubRouteRef } from '../../../frontend-plugin-api/src/routing/SubRouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExternalRouteRef } from '../../../frontend-plugin-api/src/routing/ExternalRouteRef';
|
||||
|
||||
// TODO(Rugvip): Once this is moved to a compat package these aliases can be removed and imported from frontend- instead
|
||||
|
||||
/** @ignore */
|
||||
type NewRouteRef<TParams extends AnyRouteRefParams = AnyRouteRefParams> =
|
||||
RouteRef<TParams>;
|
||||
|
||||
/** @ignore */
|
||||
type NewSubRouteRef<TParams extends AnyRouteRefParams = AnyRouteRefParams> =
|
||||
SubRouteRef<TParams>;
|
||||
|
||||
/** @ignore */
|
||||
type NewExternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
TOptional extends boolean = boolean,
|
||||
> = ExternalRouteRef<TParams, TOptional>;
|
||||
|
||||
/**
|
||||
* A temporary helper to convert a legacy route ref to the new system.
|
||||
*
|
||||
* @public
|
||||
* @remarks
|
||||
*
|
||||
* In the future the legacy createRouteRef will instead create refs compatible with both systems.
|
||||
*/
|
||||
export function convertLegacyRouteRef<TParams extends AnyRouteRefParams>(
|
||||
ref: LegacyRouteRef<TParams>,
|
||||
): NewRouteRef<TParams>;
|
||||
|
||||
/**
|
||||
* A temporary helper to convert a legacy sub route ref to the new system.
|
||||
*
|
||||
* @public
|
||||
* @remarks
|
||||
*
|
||||
* In the future the legacy createSubRouteRef will instead create refs compatible with both systems.
|
||||
*/
|
||||
export function convertLegacyRouteRef<TParams extends AnyRouteRefParams>(
|
||||
ref: LegacySubRouteRef<TParams>,
|
||||
): NewSubRouteRef<TParams>;
|
||||
|
||||
/**
|
||||
* A temporary helper to convert a legacy external route ref to the new system.
|
||||
*
|
||||
* @public
|
||||
* @remarks
|
||||
*
|
||||
* In the future the legacy createExternalRouteRef will instead create refs compatible with both systems.
|
||||
*/
|
||||
export function convertLegacyRouteRef<
|
||||
TParams extends AnyRouteRefParams,
|
||||
TOptional extends boolean,
|
||||
>(
|
||||
ref: LegacyExternalRouteRef<TParams, TOptional>,
|
||||
): NewExternalRouteRef<TParams, TOptional>;
|
||||
|
||||
export function convertLegacyRouteRef(
|
||||
ref: LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef,
|
||||
): NewRouteRef | NewSubRouteRef | NewExternalRouteRef {
|
||||
// Ref has already been converted
|
||||
if ('$$type' in ref) {
|
||||
return ref as unknown as NewRouteRef | NewSubRouteRef | NewExternalRouteRef;
|
||||
}
|
||||
|
||||
const type = (ref as unknown as { [routeRefType]: unknown })[routeRefType];
|
||||
|
||||
if (type === 'absolute') {
|
||||
const legacyRef = ref as LegacyRouteRef;
|
||||
const newRef = toInternalRouteRef(
|
||||
createRouteRef<{ [key in string]: string }>({
|
||||
params: legacyRef.params as string[],
|
||||
}),
|
||||
);
|
||||
return Object.assign(legacyRef, {
|
||||
$$type: '@backstage/RouteRef' as const,
|
||||
version: 'v1',
|
||||
T: newRef.T,
|
||||
getParams() {
|
||||
return newRef.getParams();
|
||||
},
|
||||
getDescription() {
|
||||
return newRef.getDescription();
|
||||
},
|
||||
setId(id: string) {
|
||||
newRef.setId(id);
|
||||
},
|
||||
toString() {
|
||||
return newRef.toString();
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === 'sub') {
|
||||
const legacyRef = ref as LegacySubRouteRef;
|
||||
const newRef = toInternalSubRouteRef(
|
||||
createSubRouteRef({
|
||||
path: legacyRef.path,
|
||||
parent: convertLegacyRouteRef(legacyRef.parent),
|
||||
}),
|
||||
);
|
||||
return Object.assign(legacyRef, {
|
||||
$$type: '@backstage/SubRouteRef' as const,
|
||||
version: 'v1',
|
||||
T: newRef.T,
|
||||
getParams() {
|
||||
return newRef.getParams();
|
||||
},
|
||||
getParent() {
|
||||
return newRef.getParent();
|
||||
},
|
||||
getDescription() {
|
||||
return newRef.getDescription();
|
||||
},
|
||||
toString() {
|
||||
return newRef.toString();
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === 'external') {
|
||||
const legacyRef = ref as LegacyExternalRouteRef;
|
||||
const newRef = toInternalExternalRouteRef(
|
||||
createExternalRouteRef<{ [key in string]: string }>({
|
||||
params: legacyRef.params as string[],
|
||||
optional: legacyRef.optional,
|
||||
}),
|
||||
);
|
||||
return Object.assign(legacyRef, {
|
||||
$$type: '@backstage/ExternalRouteRef' as const,
|
||||
version: 'v1',
|
||||
T: newRef.T,
|
||||
optional: newRef.optional,
|
||||
getParams() {
|
||||
return newRef.getParams();
|
||||
},
|
||||
getDescription() {
|
||||
return newRef.getDescription();
|
||||
},
|
||||
setId(id: string) {
|
||||
newRef.setId(id);
|
||||
},
|
||||
toString() {
|
||||
return newRef.toString();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Failed to convert legacy route ref, unknown type '${type}'`);
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
export type {
|
||||
AnyParams,
|
||||
AnyRouteRefParams,
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
|
||||
@@ -21,12 +21,19 @@ import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AnyParams = { [param in string]: string } | undefined;
|
||||
export type AnyRouteRefParams = { [param in string]: string } | undefined;
|
||||
|
||||
/**
|
||||
* @deprecated use {@link AnyRouteRefParams} instead
|
||||
* @public
|
||||
*/
|
||||
export type AnyParams = AnyRouteRefParams;
|
||||
|
||||
/**
|
||||
* Type describing the key type of a route parameter mapping.
|
||||
*
|
||||
* @public
|
||||
* @deprecated this type is deprecated and will be removed in the future
|
||||
*/
|
||||
export type ParamKeys<Params extends AnyParams> = keyof Params extends never
|
||||
? []
|
||||
@@ -36,6 +43,7 @@ export type ParamKeys<Params extends AnyParams> = keyof Params extends never
|
||||
* Optional route params.
|
||||
*
|
||||
* @public
|
||||
* @deprecated this type is deprecated and will be removed in the future
|
||||
*/
|
||||
export type OptionalParams<Params extends { [param in string]: string }> =
|
||||
Params[keyof Params] extends never ? undefined : Params;
|
||||
@@ -81,8 +89,10 @@ export const routeRefType: unique symbol = getOrCreateGlobalSingleton<any>(
|
||||
* @public
|
||||
*/
|
||||
export type RouteRef<Params extends AnyParams = any> = {
|
||||
/** @deprecated access to this property will be removed in the future */
|
||||
$$routeRefType: 'absolute'; // See routeRefType above
|
||||
|
||||
/** @deprecated access to this property will be removed in the future */
|
||||
params: ParamKeys<Params>;
|
||||
};
|
||||
|
||||
@@ -96,12 +106,15 @@ export type RouteRef<Params extends AnyParams = any> = {
|
||||
* @public
|
||||
*/
|
||||
export type SubRouteRef<Params extends AnyParams = any> = {
|
||||
/** @deprecated access to this property will be removed in the future */
|
||||
$$routeRefType: 'sub'; // See routeRefType above
|
||||
|
||||
/** @deprecated access to this property will be removed in the future */
|
||||
parent: RouteRef;
|
||||
|
||||
path: string;
|
||||
|
||||
/** @deprecated access to this property will be removed in the future */
|
||||
params: ParamKeys<Params>;
|
||||
};
|
||||
|
||||
@@ -118,8 +131,10 @@ export type ExternalRouteRef<
|
||||
Params extends AnyParams = any,
|
||||
Optional extends boolean = any,
|
||||
> = {
|
||||
/** @deprecated access to this property will be removed in the future */
|
||||
$$routeRefType: 'external'; // See routeRefType above
|
||||
|
||||
/** @deprecated access to this property will be removed in the future */
|
||||
params: ParamKeys<Params>;
|
||||
|
||||
optional?: Optional;
|
||||
|
||||
@@ -3,13 +3,28 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AppRouteBinder } from '@backstage/core-app-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionOverrides } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { SubRouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @public
|
||||
export type AppRouteBinder = <
|
||||
TExternalRoutes extends {
|
||||
[name: string]: ExternalRouteRef;
|
||||
},
|
||||
>(
|
||||
externalRoutes: TExternalRoutes,
|
||||
targetRoutes: PartialKeys<
|
||||
TargetRouteMap<TExternalRoutes>,
|
||||
KeysWithType<TExternalRoutes, ExternalRouteRef<any, true>>
|
||||
>,
|
||||
) => void;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createApp(options: {
|
||||
|
||||
+7
@@ -24,6 +24,13 @@ export interface Config {
|
||||
packages?: 'all' | { include?: string[]; exclude?: string[] };
|
||||
};
|
||||
|
||||
routes?: {
|
||||
/**
|
||||
* @deepVisibility frontend
|
||||
*/
|
||||
bindings?: { [externalRouteRefId: string]: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* @deepVisibility frontend
|
||||
*/
|
||||
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
NavTarget,
|
||||
useRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -66,7 +66,7 @@ const SidebarLogo = () => {
|
||||
|
||||
const SidebarNavItem = (props: NavTarget) => {
|
||||
const { icon: Icon, title, routeRef } = props;
|
||||
const to = useRouteRef(routeRef)({});
|
||||
const to = useRouteRef(routeRef)();
|
||||
// TODO: Support opening modal, for example, the search one
|
||||
return <SidebarItem to={to} icon={Icon} text={title} />;
|
||||
};
|
||||
|
||||
@@ -21,3 +21,4 @@
|
||||
*/
|
||||
|
||||
export * from './wiring';
|
||||
export * from './routing';
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
/*
|
||||
* Copyright 2020 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 {
|
||||
createRouteRef,
|
||||
createSubRouteRef,
|
||||
createExternalRouteRef,
|
||||
ExternalRouteRef,
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { RouteResolver } from './RouteResolver';
|
||||
import { MATCH_ALL_ROUTE } from './extractRouteInfoFromInstanceTree';
|
||||
|
||||
const element = () => null;
|
||||
const rest = {
|
||||
element,
|
||||
caseSensitive: false,
|
||||
children: [MATCH_ALL_ROUTE],
|
||||
plugins: new Set<BackstagePlugin>(),
|
||||
};
|
||||
|
||||
const ref1 = createRouteRef();
|
||||
const ref2 = createRouteRef({ params: ['x'] });
|
||||
const ref3 = createRouteRef({ params: ['y'] });
|
||||
const subRef1 = createSubRouteRef({ parent: ref1, path: '/foo' });
|
||||
const subRef2 = createSubRouteRef({ parent: ref1, path: '/foo/:a' });
|
||||
const subRef3 = createSubRouteRef({ parent: ref2, path: '/bar' });
|
||||
const subRef4 = createSubRouteRef({ parent: ref2, path: '/bar/:a' });
|
||||
const externalRef1 = createExternalRouteRef();
|
||||
const externalRef2 = createExternalRouteRef({ optional: true });
|
||||
const externalRef3 = createExternalRouteRef({ params: ['x'] });
|
||||
const externalRef4 = createExternalRouteRef({ optional: true, params: ['x'] });
|
||||
|
||||
describe('RouteResolver', () => {
|
||||
it('should not resolve anything with an empty resolver', () => {
|
||||
const r = new RouteResolver(new Map(), new Map(), [], new Map(), '');
|
||||
|
||||
expect(r.resolve(ref1, '/')?.()).toBe(undefined);
|
||||
expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined);
|
||||
expect(r.resolve(subRef1, '/')?.()).toBe(undefined);
|
||||
expect(r.resolve(subRef2, '/')?.({ a: '2a' })).toBe(undefined);
|
||||
expect(r.resolve(subRef3, '/')?.({ x: '3x' })).toBe(undefined);
|
||||
expect(r.resolve(subRef4, '/')?.({ x: '4x', a: '4a' })).toBe(undefined);
|
||||
expect(r.resolve(externalRef1, '/')?.()).toBe(undefined);
|
||||
expect(r.resolve(externalRef2, '/')?.()).toBe(undefined);
|
||||
expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe(undefined);
|
||||
expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should resolve an absolute route', () => {
|
||||
const r = new RouteResolver(
|
||||
new Map([[ref1, 'my-route']]),
|
||||
new Map(),
|
||||
[{ routeRefs: new Set([ref1]), path: 'my-route', ...rest }],
|
||||
new Map(),
|
||||
'',
|
||||
);
|
||||
|
||||
expect(r.resolve(ref1, '/')?.()).toBe('/my-route');
|
||||
expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined);
|
||||
expect(r.resolve(subRef1, '/')?.()).toBe('/my-route/foo');
|
||||
expect(r.resolve(subRef2, '/')?.({ a: '2a' })).toBe('/my-route/foo/2a');
|
||||
expect(r.resolve(subRef3, '/')?.({ x: '3x' })).toBe(undefined);
|
||||
expect(r.resolve(subRef4, '/')?.({ x: '4x', a: '4a' })).toBe(undefined);
|
||||
expect(r.resolve(externalRef1, '/')?.()).toBe(undefined);
|
||||
expect(r.resolve(externalRef2, '/')?.()).toBe(undefined);
|
||||
expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe(undefined);
|
||||
expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should resolve an absolute route and sub route with an app base path', () => {
|
||||
const r = new RouteResolver(
|
||||
new Map<RouteRef, string>([
|
||||
[ref2, 'my-parent/:x'],
|
||||
[ref1, 'my-route'],
|
||||
]),
|
||||
new Map<RouteRef, RouteRef>([[ref1, ref2]]),
|
||||
[
|
||||
{
|
||||
routeRefs: new Set([ref2]),
|
||||
path: 'my-parent/:x',
|
||||
...rest,
|
||||
children: [
|
||||
MATCH_ALL_ROUTE,
|
||||
{ routeRefs: new Set([ref1]), path: 'my-route', ...rest },
|
||||
],
|
||||
},
|
||||
],
|
||||
new Map(),
|
||||
'/base',
|
||||
);
|
||||
|
||||
expect(r.resolve(ref1, '/my-parent/1x')?.()).toBe(
|
||||
'/base/my-parent/1x/my-route',
|
||||
);
|
||||
expect(r.resolve(ref1, '/base/my-parent/1x')?.()).toBe(
|
||||
'/base/my-parent/1x/my-route',
|
||||
);
|
||||
expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe('/base/my-parent/1x');
|
||||
expect(r.resolve(ref2, '/base')?.({ x: '1x' })).toBe('/base/my-parent/1x');
|
||||
expect(r.resolve(ref3, '/')?.({ y: '1y' })).toBe(undefined);
|
||||
expect(r.resolve(subRef1, '/my-parent/2x')?.()).toBe(
|
||||
'/base/my-parent/2x/my-route/foo',
|
||||
);
|
||||
expect(r.resolve(subRef1, '/base/my-parent/2x')?.()).toBe(
|
||||
'/base/my-parent/2x/my-route/foo',
|
||||
);
|
||||
expect(r.resolve(subRef2, '/my-parent/3x')?.({ a: '2a' })).toBe(
|
||||
'/base/my-parent/3x/my-route/foo/2a',
|
||||
);
|
||||
expect(r.resolve(subRef2, '/base/my-parent/3x')?.({ a: '2a' })).toBe(
|
||||
'/base/my-parent/3x/my-route/foo/2a',
|
||||
);
|
||||
expect(r.resolve(subRef3, '/')?.({ x: '5x' })).toBe(
|
||||
'/base/my-parent/5x/bar',
|
||||
);
|
||||
expect(r.resolve(subRef4, '/')?.({ x: '6x', a: '4a' })).toBe(
|
||||
'/base/my-parent/6x/bar/4a',
|
||||
);
|
||||
expect(r.resolve(externalRef1, '/')?.()).toBe(undefined);
|
||||
expect(r.resolve(externalRef2, '/')?.()).toBe(undefined);
|
||||
expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe(undefined);
|
||||
expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should resolve an absolute route with a param and with a parent', () => {
|
||||
const r = new RouteResolver(
|
||||
new Map<RouteRef, string>([
|
||||
[ref1, 'my-route'],
|
||||
[ref2, 'my-parent/:x'],
|
||||
]),
|
||||
new Map([[ref2, ref1]]),
|
||||
[
|
||||
{
|
||||
routeRefs: new Set([ref2]),
|
||||
path: 'my-parent/:x',
|
||||
...rest,
|
||||
children: [
|
||||
MATCH_ALL_ROUTE,
|
||||
{ routeRefs: new Set([ref1]), path: 'my-route', ...rest },
|
||||
],
|
||||
},
|
||||
],
|
||||
new Map<ExternalRouteRef, RouteRef | SubRouteRef>([
|
||||
[externalRef1, ref1],
|
||||
[externalRef3, ref2],
|
||||
[externalRef4, subRef3],
|
||||
]),
|
||||
'',
|
||||
);
|
||||
|
||||
expect(r.resolve(ref1, '/')?.()).toBe('/my-route');
|
||||
expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe('/my-route/my-parent/1x');
|
||||
expect(r.resolve(subRef1, '/')?.()).toBe('/my-route/foo');
|
||||
expect(r.resolve(subRef2, '/')?.({ a: '2a' })).toBe('/my-route/foo/2a');
|
||||
expect(r.resolve(subRef3, '/')?.({ x: '3x' })).toBe(
|
||||
'/my-route/my-parent/3x/bar',
|
||||
);
|
||||
expect(r.resolve(subRef4, '/')?.({ x: '4x', a: '4a' })).toBe(
|
||||
'/my-route/my-parent/4x/bar/4a',
|
||||
);
|
||||
expect(r.resolve(externalRef1, '/')?.()).toBe('/my-route');
|
||||
expect(r.resolve(externalRef2, '/')?.()).toBe(undefined);
|
||||
expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe(
|
||||
'/my-route/my-parent/5x',
|
||||
);
|
||||
expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(
|
||||
'/my-route/my-parent/6x/bar',
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve the most specific match', () => {
|
||||
const r = new RouteResolver(
|
||||
new Map<RouteRef, string>([
|
||||
[ref1, 'deep'],
|
||||
[ref2, 'root/:x'],
|
||||
[ref3, 'sub/:y'],
|
||||
]),
|
||||
new Map<RouteRef, RouteRef>([
|
||||
[ref3, ref2],
|
||||
[ref1, ref3],
|
||||
]),
|
||||
[
|
||||
{
|
||||
routeRefs: new Set([ref2]),
|
||||
path: 'root/:x',
|
||||
...rest,
|
||||
children: [
|
||||
MATCH_ALL_ROUTE,
|
||||
{
|
||||
routeRefs: new Set([ref3]),
|
||||
path: 'sub/:y',
|
||||
...rest,
|
||||
children: [
|
||||
MATCH_ALL_ROUTE,
|
||||
{
|
||||
routeRefs: new Set([ref1]),
|
||||
path: 'deep',
|
||||
...rest,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
new Map<ExternalRouteRef, RouteRef | SubRouteRef>(),
|
||||
'',
|
||||
);
|
||||
|
||||
expect(r.resolve(ref2, '/')?.({ x: 'x' })).toBe('/root/x');
|
||||
expect(r.resolve(ref3, '/root/x')?.({ y: 'y' })).toBe('/root/x/sub/y');
|
||||
|
||||
expect(() => r.resolve(ref1, '/')?.()).toThrow(
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
expect(() => r.resolve(ref1, '/root/x')?.()).toThrow(
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
expect(r.resolve(ref1, '/root/x/sub/y')?.()).toBe('/root/x/sub/y/deep');
|
||||
// Without the MATCH_ALL_ROUTE, we wouldn't properly match the route here
|
||||
expect(r.resolve(ref1, '/root/x/sub/y/any/nested/path/here')?.()).toBe(
|
||||
'/root/x/sub/y/deep',
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve an absolute route with multiple parents', () => {
|
||||
const r = new RouteResolver(
|
||||
new Map<RouteRef, string>([
|
||||
[ref1, 'my-route'],
|
||||
[ref2, 'my-parent/:x'],
|
||||
[ref3, 'my-grandparent/:y'],
|
||||
]),
|
||||
new Map<RouteRef, RouteRef>([
|
||||
[ref1, ref2],
|
||||
[ref2, ref3],
|
||||
]),
|
||||
[
|
||||
{
|
||||
routeRefs: new Set([ref3]),
|
||||
path: 'my-grandparent/:y',
|
||||
...rest,
|
||||
children: [
|
||||
MATCH_ALL_ROUTE,
|
||||
{
|
||||
routeRefs: new Set([ref2]),
|
||||
path: 'my-parent/:x',
|
||||
...rest,
|
||||
children: [
|
||||
MATCH_ALL_ROUTE,
|
||||
{ routeRefs: new Set([ref1]), path: 'my-route', ...rest },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
new Map<ExternalRouteRef, RouteRef | SubRouteRef>([
|
||||
[externalRef1, ref1],
|
||||
[externalRef3, ref2],
|
||||
[externalRef4, subRef3],
|
||||
]),
|
||||
'',
|
||||
);
|
||||
|
||||
const l = '/my-grandparent/my-y/my-parent/my-x';
|
||||
expect(r.resolve(ref1, l)?.()).toBe(
|
||||
'/my-grandparent/my-y/my-parent/my-x/my-route',
|
||||
);
|
||||
expect(() => r.resolve(ref1, '/')?.()).toThrow(
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
expect(r.resolve(ref2, l)?.({ x: '1x' })).toBe(
|
||||
'/my-grandparent/my-y/my-parent/1x',
|
||||
);
|
||||
expect(r.resolve(ref2, '/my-grandparent/my-y')?.({ x: '1x' })).toBe(
|
||||
'/my-grandparent/my-y/my-parent/1x',
|
||||
);
|
||||
expect(() => r.resolve(ref2, '/')?.({ x: '1x' })).toThrow(
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
expect(r.resolve(subRef1, l)?.()).toBe(
|
||||
'/my-grandparent/my-y/my-parent/my-x/my-route/foo',
|
||||
);
|
||||
expect(() => r.resolve(subRef1, '/')?.()).toThrow(
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
expect(r.resolve(subRef2, l)?.({ a: '2a' })).toBe(
|
||||
'/my-grandparent/my-y/my-parent/my-x/my-route/foo/2a',
|
||||
);
|
||||
expect(() => r.resolve(subRef2, '/')?.({ a: '2a' })).toThrow(
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
expect(r.resolve(subRef3, l)?.({ x: '3x' })).toBe(
|
||||
'/my-grandparent/my-y/my-parent/3x/bar',
|
||||
);
|
||||
expect(r.resolve(subRef3, '/my-grandparent/my-y')?.({ x: '3x' })).toBe(
|
||||
'/my-grandparent/my-y/my-parent/3x/bar',
|
||||
);
|
||||
expect(r.resolve(subRef4, l)?.({ x: '4x', a: '4a' })).toBe(
|
||||
'/my-grandparent/my-y/my-parent/4x/bar/4a',
|
||||
);
|
||||
expect(
|
||||
r.resolve(subRef4, '/my-grandparent/my-y')?.({ x: '4x', a: '4a' }),
|
||||
).toBe('/my-grandparent/my-y/my-parent/4x/bar/4a');
|
||||
expect(r.resolve(externalRef1, l)?.()).toBe(
|
||||
'/my-grandparent/my-y/my-parent/my-x/my-route',
|
||||
);
|
||||
expect(() => r.resolve(externalRef1, '/')?.()).toThrow(
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
expect(r.resolve(externalRef2, l)?.()).toBe(undefined);
|
||||
expect(r.resolve(externalRef3, l)?.({ x: '5x' })).toBe(
|
||||
'/my-grandparent/my-y/my-parent/5x',
|
||||
);
|
||||
expect(() => r.resolve(externalRef3, '/')?.({ x: '5x' })).toThrow(
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
expect(r.resolve(externalRef4, l)?.({ x: '6x' })).toBe(
|
||||
'/my-grandparent/my-y/my-parent/6x/bar',
|
||||
);
|
||||
expect(() => r.resolve(externalRef4, '/')?.({ x: '6x' })).toThrow(
|
||||
/^Cannot route.*with parent.*as it has parameters$/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode some characters in params', () => {
|
||||
const r = new RouteResolver(
|
||||
new Map<RouteRef, string>([
|
||||
[ref2, 'my-parent/:x'],
|
||||
[ref1, 'my-route'],
|
||||
]),
|
||||
new Map<RouteRef, RouteRef>([[ref1, ref2]]),
|
||||
[
|
||||
{
|
||||
routeRefs: new Set([ref2]),
|
||||
path: 'my-parent/:x',
|
||||
...rest,
|
||||
children: [
|
||||
MATCH_ALL_ROUTE,
|
||||
{ routeRefs: new Set([ref1]), path: 'my-route', ...rest },
|
||||
],
|
||||
},
|
||||
],
|
||||
new Map(),
|
||||
'/base',
|
||||
);
|
||||
|
||||
expect(r.resolve(ref2, '/')?.({ x: 'a/#&?b' })).toBe(
|
||||
'/base/my-parent/a%2F%23%26%3Fb',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Copyright 2020 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 { generatePath, matchRoutes } from 'react-router-dom';
|
||||
import {
|
||||
RouteRef,
|
||||
ExternalRouteRef,
|
||||
SubRouteRef,
|
||||
AnyRouteRefParams,
|
||||
RouteFunc,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import { AnyRouteRef, BackstageRouteObject } from './types';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { isRouteRef } from '../../../frontend-plugin-api/src/routing/RouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
isSubRouteRef,
|
||||
toInternalSubRouteRef,
|
||||
} from '../../../frontend-plugin-api/src/routing/SubRouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { isExternalRouteRef } from '../../../frontend-plugin-api/src/routing/ExternalRouteRef';
|
||||
|
||||
// Joins a list of paths together, avoiding trailing and duplicate slashes
|
||||
export function joinPaths(...paths: string[]): string {
|
||||
const normalized = paths.join('/').replace(/\/\/+/g, '/');
|
||||
if (normalized !== '/' && normalized.endsWith('/')) {
|
||||
return normalized.slice(0, -1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the absolute route ref that our target route ref is pointing pointing to, as well
|
||||
* as the relative target path.
|
||||
*
|
||||
* Returns an undefined target ref if one could not be fully resolved.
|
||||
*/
|
||||
function resolveTargetRef(
|
||||
anyRouteRef: AnyRouteRef,
|
||||
routePaths: Map<RouteRef, string>,
|
||||
routeBindings: Map<AnyRouteRef, AnyRouteRef | undefined>,
|
||||
): readonly [RouteRef | undefined, string] {
|
||||
// First we figure out which absolute route ref we're dealing with, an if there was an sub route path to append.
|
||||
// For sub routes it will be the parent path, while for external routes it will be the bound route.
|
||||
let targetRef: RouteRef;
|
||||
let subRoutePath = '';
|
||||
if (isRouteRef(anyRouteRef)) {
|
||||
targetRef = anyRouteRef;
|
||||
} else if (isSubRouteRef(anyRouteRef)) {
|
||||
const internal = toInternalSubRouteRef(anyRouteRef);
|
||||
targetRef = internal.getParent();
|
||||
subRoutePath = internal.path;
|
||||
} else if (isExternalRouteRef(anyRouteRef)) {
|
||||
const resolvedRoute = routeBindings.get(anyRouteRef);
|
||||
if (!resolvedRoute) {
|
||||
return [undefined, ''];
|
||||
}
|
||||
if (isRouteRef(resolvedRoute)) {
|
||||
targetRef = resolvedRoute;
|
||||
} else if (isSubRouteRef(resolvedRoute)) {
|
||||
const internal = toInternalSubRouteRef(resolvedRoute);
|
||||
targetRef = internal.getParent();
|
||||
subRoutePath = resolvedRoute.path;
|
||||
} else {
|
||||
throw new Error(
|
||||
`ExternalRouteRef was bound to invalid target, ${resolvedRoute}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown object passed to useRouteRef, got ${anyRouteRef}`);
|
||||
}
|
||||
|
||||
// Bail if no absolute path could be resolved
|
||||
if (!targetRef) {
|
||||
return [undefined, ''];
|
||||
}
|
||||
|
||||
// Find the path that our target route is bound to
|
||||
const resolvedPath = routePaths.get(targetRef);
|
||||
if (resolvedPath === undefined) {
|
||||
return [undefined, ''];
|
||||
}
|
||||
|
||||
// SubRouteRefs join the path from the parent route with its own path
|
||||
const targetPath = joinPaths(resolvedPath, subRoutePath);
|
||||
return [targetRef, targetPath];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the complete base path for navigating to the target RouteRef.
|
||||
*/
|
||||
function resolveBasePath(
|
||||
targetRef: RouteRef,
|
||||
sourceLocation: Parameters<typeof matchRoutes>[1],
|
||||
routePaths: Map<RouteRef, string>,
|
||||
routeParents: Map<RouteRef, RouteRef | undefined>,
|
||||
routeObjects: BackstageRouteObject[],
|
||||
) {
|
||||
// While traversing the app element tree we build up the routeObjects structure
|
||||
// used here. It is the same kind of structure that react-router creates, with the
|
||||
// addition that associated route refs are stored throughout the tree. This lets
|
||||
// us look up all route refs that can be reached from our source location.
|
||||
// Because of the similar route object structure, we can use `matchRoutes` from
|
||||
// react-router to do the lookup of our current location.
|
||||
const match = matchRoutes(routeObjects, sourceLocation) ?? [];
|
||||
|
||||
// While we search for a common routing root between our current location and
|
||||
// the target route, we build a list of all route refs we find that we need
|
||||
// to traverse to reach the target.
|
||||
const refDiffList = Array<RouteRef>();
|
||||
|
||||
let matchIndex = -1;
|
||||
for (
|
||||
let targetSearchRef: RouteRef | undefined = targetRef;
|
||||
targetSearchRef;
|
||||
targetSearchRef = routeParents.get(targetSearchRef)
|
||||
) {
|
||||
// The match contains a list of all ancestral route refs present at our current location
|
||||
// Starting at the desired target ref and traversing back through its parents, we search
|
||||
// for a target ref that is present in the match for our current location. When a match
|
||||
// is found it means we have found a common base to resolve the route from.
|
||||
matchIndex = match.findIndex(m =>
|
||||
(m.route as BackstageRouteObject).routeRefs.has(targetSearchRef!),
|
||||
);
|
||||
if (matchIndex !== -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Every time we move a step up in the ancestry of the target ref, we add the current ref
|
||||
// to the diff list, which ends up being the list of route refs to traverse form the common base
|
||||
// in order to reach our target.
|
||||
refDiffList.unshift(targetSearchRef);
|
||||
}
|
||||
|
||||
// If our target route is present in the initial match we need to construct the final path
|
||||
// from the parent of the matched route segment. That's to allow the caller of the route
|
||||
// function to supply their own params.
|
||||
if (refDiffList.length === 0) {
|
||||
matchIndex -= 1;
|
||||
}
|
||||
|
||||
// This is the part of the route tree that the target and source locations have in common.
|
||||
// We re-use the existing pathname directly along with all params.
|
||||
const parentPath = matchIndex === -1 ? '' : match[matchIndex].pathname;
|
||||
|
||||
// This constructs the mid section of the path using paths resolved from all route refs
|
||||
// we need to traverse to reach our target except for the very last one. None of these
|
||||
// paths are allowed to require any parameters, as the caller would have no way of knowing
|
||||
// what parameters those are.
|
||||
const diffPaths = refDiffList.slice(0, -1).map(ref => {
|
||||
const path = routePaths.get(ref);
|
||||
if (path === undefined) {
|
||||
throw new Error(`No path for ${ref}`);
|
||||
}
|
||||
if (path.includes(':')) {
|
||||
throw new Error(
|
||||
`Cannot route to ${targetRef} with parent ${ref} as it has parameters`,
|
||||
);
|
||||
}
|
||||
return path;
|
||||
});
|
||||
|
||||
return `${joinPaths(parentPath, ...diffPaths)}/`;
|
||||
}
|
||||
|
||||
export class RouteResolver {
|
||||
constructor(
|
||||
private readonly routePaths: Map<RouteRef, string>,
|
||||
private readonly routeParents: Map<RouteRef, RouteRef | undefined>,
|
||||
private readonly routeObjects: BackstageRouteObject[],
|
||||
private readonly routeBindings: Map<
|
||||
ExternalRouteRef,
|
||||
RouteRef | SubRouteRef
|
||||
>,
|
||||
private readonly appBasePath: string, // base path without a trailing slash
|
||||
) {}
|
||||
|
||||
resolve<Params extends AnyRouteRefParams>(
|
||||
anyRouteRef:
|
||||
| RouteRef<Params>
|
||||
| SubRouteRef<Params>
|
||||
| ExternalRouteRef<Params, any>,
|
||||
sourceLocation: Parameters<typeof matchRoutes>[1],
|
||||
): RouteFunc<Params> | undefined {
|
||||
// First figure out what our target absolute ref is, as well as our target path.
|
||||
const [targetRef, targetPath] = resolveTargetRef(
|
||||
anyRouteRef,
|
||||
this.routePaths,
|
||||
this.routeBindings,
|
||||
);
|
||||
if (!targetRef) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// The location that we get passed in uses the full path, so start by trimming off
|
||||
// the app base path prefix in case we're running the app on a sub-path.
|
||||
let relativeSourceLocation: Parameters<typeof matchRoutes>[1];
|
||||
if (typeof sourceLocation === 'string') {
|
||||
relativeSourceLocation = this.trimPath(sourceLocation);
|
||||
} else if (sourceLocation.pathname) {
|
||||
relativeSourceLocation = {
|
||||
...sourceLocation,
|
||||
pathname: this.trimPath(sourceLocation.pathname),
|
||||
};
|
||||
} else {
|
||||
relativeSourceLocation = sourceLocation;
|
||||
}
|
||||
|
||||
// Next we figure out the base path, which is the combination of the common parent path
|
||||
// between our current location and our target location, as well as the additional path
|
||||
// that is the difference between the parent path and the base of our target location.
|
||||
const basePath =
|
||||
this.appBasePath +
|
||||
resolveBasePath(
|
||||
targetRef,
|
||||
relativeSourceLocation,
|
||||
this.routePaths,
|
||||
this.routeParents,
|
||||
this.routeObjects,
|
||||
);
|
||||
|
||||
const routeFunc: RouteFunc<Params> = (...[params]) => {
|
||||
// We selectively encode some some known-dangerous characters in the
|
||||
// params. The reason that we don't perform a blanket `encodeURIComponent`
|
||||
// here is that this encoding was added defensively long after the initial
|
||||
// release of this code. There's likely to be many users of this code that
|
||||
// already encode their parameters knowing that this code didn't do this
|
||||
// for them in the past. Therefore, we are extra careful NOT to include
|
||||
// the percent character in this set, even though that might seem like a
|
||||
// bad idea.
|
||||
const encodedParams =
|
||||
params &&
|
||||
mapValues(params, value => {
|
||||
if (typeof value === 'string') {
|
||||
return value.replaceAll(/[&?#;\/]/g, c => encodeURIComponent(c));
|
||||
}
|
||||
return value;
|
||||
});
|
||||
return joinPaths(basePath, generatePath(targetPath, encodedParams));
|
||||
};
|
||||
return routeFunc;
|
||||
}
|
||||
|
||||
private trimPath(targetPath: string) {
|
||||
if (!targetPath) {
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
if (targetPath.startsWith(this.appBasePath)) {
|
||||
return targetPath.slice(this.appBasePath.length);
|
||||
}
|
||||
return targetPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2020 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, { ReactNode } from 'react';
|
||||
import {
|
||||
ExternalRouteRef,
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
createVersionedValueMap,
|
||||
createVersionedContext,
|
||||
} from '@backstage/version-bridge';
|
||||
import { RouteResolver } from './RouteResolver';
|
||||
import { BackstageRouteObject } from './types';
|
||||
|
||||
const RoutingContext = createVersionedContext<{ 1: RouteResolver }>(
|
||||
'routing-context',
|
||||
);
|
||||
|
||||
type ProviderProps = {
|
||||
routePaths: Map<RouteRef, string>;
|
||||
routeParents: Map<RouteRef, RouteRef | undefined>;
|
||||
routeObjects: BackstageRouteObject[];
|
||||
routeBindings: Map<ExternalRouteRef, RouteRef | SubRouteRef>;
|
||||
basePath?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// TODO(Rugvip): Migrate to a routing API instead
|
||||
export const RoutingProvider = ({
|
||||
routePaths,
|
||||
routeParents,
|
||||
routeObjects,
|
||||
routeBindings,
|
||||
basePath = '',
|
||||
children,
|
||||
}: ProviderProps) => {
|
||||
const resolver = new RouteResolver(
|
||||
routePaths,
|
||||
routeParents,
|
||||
routeObjects,
|
||||
routeBindings,
|
||||
basePath,
|
||||
);
|
||||
|
||||
const versionedValue = createVersionedValueMap({ 1: resolver });
|
||||
return (
|
||||
<RoutingContext.Provider value={versionedValue}>
|
||||
{children}
|
||||
</RoutingContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 {
|
||||
createRouteRef,
|
||||
createExternalRouteRef,
|
||||
createPlugin,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { collectRouteIds } from './collectRouteIds';
|
||||
|
||||
describe('collectRouteIds', () => {
|
||||
it('should assign IDs to routes', () => {
|
||||
const ref = createRouteRef();
|
||||
const extRef = createExternalRouteRef();
|
||||
|
||||
expect(String(ref)).toMatch(
|
||||
/^RouteRef\{created at '.*collectRouteIds\.test\.ts.*'\}$/,
|
||||
);
|
||||
expect(String(extRef)).toMatch(
|
||||
/^ExternalRouteRef\{created at '.*collectRouteIds\.test\.ts.*'\}$/,
|
||||
);
|
||||
|
||||
const collected = collectRouteIds([
|
||||
createPlugin({ id: 'test', routes: { ref }, externalRoutes: { extRef } }),
|
||||
]);
|
||||
expect(Object.fromEntries(collected.routes)).toEqual({
|
||||
'plugin.test.routes.ref': ref,
|
||||
});
|
||||
expect(Object.fromEntries(collected.externalRoutes)).toEqual({
|
||||
'plugin.test.externalRoutes.extRef': extRef,
|
||||
});
|
||||
|
||||
expect(String(ref)).toBe('RouteRef{plugin.test.routes.ref}');
|
||||
expect(String(extRef)).toBe(
|
||||
'ExternalRouteRef{plugin.test.externalRoutes.extRef}',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 {
|
||||
BackstagePlugin,
|
||||
ExtensionOverrides,
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalRouteRef } from '../../../frontend-plugin-api/src/routing/RouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExternalRouteRef } from '../../../frontend-plugin-api/src/routing/ExternalRouteRef';
|
||||
|
||||
/** @internal */
|
||||
export interface RouteRefsById {
|
||||
routes: Map<string, RouteRef | SubRouteRef>;
|
||||
externalRoutes: Map<string, ExternalRouteRef>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function collectRouteIds(
|
||||
features: (BackstagePlugin | ExtensionOverrides)[],
|
||||
): RouteRefsById {
|
||||
const routesById = new Map<string, RouteRef | SubRouteRef>();
|
||||
const externalRoutesById = new Map<string, ExternalRouteRef>();
|
||||
|
||||
for (const feature of features) {
|
||||
if (feature.$$type !== '@backstage/BackstagePlugin') {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [name, ref] of Object.entries(feature.routes)) {
|
||||
const refId = `plugin.${feature.id}.routes.${name}`;
|
||||
if (routesById.has(refId)) {
|
||||
throw new Error(`Unexpected duplicate route '${refId}'`);
|
||||
}
|
||||
|
||||
const internalRef = toInternalRouteRef(ref);
|
||||
internalRef.setId(refId);
|
||||
routesById.set(refId, ref);
|
||||
}
|
||||
for (const [name, ref] of Object.entries(feature.externalRoutes)) {
|
||||
const refId = `plugin.${feature.id}.externalRoutes.${name}`;
|
||||
if (externalRoutesById.has(refId)) {
|
||||
throw new Error(`Unexpected duplicate external route '${refId}'`);
|
||||
}
|
||||
|
||||
const internalRef = toInternalExternalRouteRef(ref);
|
||||
internalRef.setId(refId);
|
||||
externalRoutesById.set(refId, ref);
|
||||
}
|
||||
}
|
||||
|
||||
return { routes: routesById, externalRoutes: externalRoutesById };
|
||||
}
|
||||
@@ -15,28 +15,27 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
BackstagePlugin,
|
||||
RouteRef,
|
||||
createRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { extractRouteInfoFromInstanceTree } from './extractRouteInfoFromInstanceTree';
|
||||
import {
|
||||
AnyRouteRefParams,
|
||||
Extension,
|
||||
RouteRef,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionInput,
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createInstances } from '../wiring/createApp';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
|
||||
const ref1 = createRouteRef({ id: 'page1' });
|
||||
const ref2 = createRouteRef({ id: 'page2' });
|
||||
const ref3 = createRouteRef({ id: 'page3' });
|
||||
const ref4 = createRouteRef({ id: 'page4' });
|
||||
const ref5 = createRouteRef({ id: 'page5' });
|
||||
const refOrder = [ref1, ref2, ref3, ref4, ref5];
|
||||
const ref1 = createRouteRef();
|
||||
const ref2 = createRouteRef();
|
||||
const ref3 = createRouteRef();
|
||||
const ref4 = createRouteRef();
|
||||
const ref5 = createRouteRef();
|
||||
const refOrder: RouteRef<AnyRouteRefParams>[] = [ref1, ref2, ref3, ref4, ref5];
|
||||
|
||||
function createTestExtension(options: {
|
||||
id: string;
|
||||
|
||||
@@ -14,12 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { coreExtensionData } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef, coreExtensionData } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionInstance } from '../wiring/createExtensionInstance';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { BackstageRouteObject } from '../../../core-app-api/src/routing/types';
|
||||
import { toLegacyPlugin } from '../wiring/createApp';
|
||||
import { BackstageRouteObject } from './types';
|
||||
|
||||
// We always add a child that matches all subroutes but without any route refs. This makes
|
||||
// sure that we're always able to match each route no matter how deep the navigation goes.
|
||||
@@ -109,13 +108,6 @@ export function extractRouteInfoFromInstanceTree(core: ExtensionInstance): {
|
||||
|
||||
// Whenever a route ref is encountered, we need to give it a route path and position in the ref tree.
|
||||
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}'`,
|
||||
);
|
||||
}
|
||||
|
||||
// The first route ref we find after encountering a route path is selected to be used as the
|
||||
// parent ref further down the tree. We don't start using this candidate ref until we encounter
|
||||
// another route path though, at which point we repeat the process and select another candidate.
|
||||
|
||||
@@ -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 { type AppRouteBinder } from './resolveRouteBindings';
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2020 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 {
|
||||
createExternalRouteRef,
|
||||
createRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { resolveRouteBindings } from './resolveRouteBindings';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
const emptyIds = { routes: new Map(), externalRoutes: new Map() };
|
||||
|
||||
describe('resolveRouteBindings', () => {
|
||||
it('runs happy path', () => {
|
||||
const external = { myRoute: createExternalRouteRef() };
|
||||
const ref = createRouteRef();
|
||||
const result = resolveRouteBindings(
|
||||
({ bind }) => {
|
||||
bind(external, { myRoute: ref });
|
||||
},
|
||||
new ConfigReader({}),
|
||||
emptyIds,
|
||||
);
|
||||
|
||||
expect(result.get(external.myRoute)).toBe(ref);
|
||||
});
|
||||
|
||||
it('throws on unknown keys', () => {
|
||||
const external = { myRoute: createExternalRouteRef() };
|
||||
const ref = createRouteRef();
|
||||
expect(() =>
|
||||
resolveRouteBindings(
|
||||
({ bind }) => {
|
||||
bind(external, { someOtherRoute: ref } as any);
|
||||
},
|
||||
new ConfigReader({}),
|
||||
emptyIds,
|
||||
),
|
||||
).toThrow('Key someOtherRoute is not an existing external route');
|
||||
});
|
||||
|
||||
it('reads bindings from config', () => {
|
||||
const mySource = createExternalRouteRef();
|
||||
const myTarget = createRouteRef();
|
||||
const result = resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({
|
||||
app: { routes: { bindings: { mySource: 'myTarget' } } },
|
||||
}),
|
||||
{
|
||||
routes: new Map([['myTarget', myTarget]]),
|
||||
externalRoutes: new Map([['mySource', mySource]]),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.get(mySource)).toBe(myTarget);
|
||||
});
|
||||
|
||||
it('throws on invalid config', () => {
|
||||
expect(() =>
|
||||
resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({ app: { routes: { bindings: 'derp' } } }),
|
||||
emptyIds,
|
||||
),
|
||||
).toThrow(
|
||||
"Invalid type in config for key 'app.routes.bindings' in 'mock-config', got string, wanted object",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({ app: { routes: { bindings: { mySource: true } } } }),
|
||||
emptyIds,
|
||||
),
|
||||
).toThrow(
|
||||
"Invalid config at app.routes.bindings['mySource'], value must be a non-empty string",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({
|
||||
app: { routes: { bindings: { mySource: 'myTarget' } } },
|
||||
}),
|
||||
emptyIds,
|
||||
),
|
||||
).toThrow(
|
||||
"Invalid config at app.routes.bindings, 'mySource' is not a valid external route",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({
|
||||
app: { routes: { bindings: { mySource: 'myTarget' } } },
|
||||
}),
|
||||
{
|
||||
...emptyIds,
|
||||
externalRoutes: new Map([['mySource', createExternalRouteRef()]]),
|
||||
},
|
||||
),
|
||||
).toThrow(
|
||||
"Invalid config at app.routes.bindings['mySource'], 'myTarget' is not a valid route",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2020 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,
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { RouteRefsById } from './collectRouteIds';
|
||||
import { Config } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
|
||||
/**
|
||||
* Extracts a union of the keys in a map whose value extends the given type
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
type KeysWithType<Obj extends { [key in string]: any }, Type> = {
|
||||
[key in keyof Obj]: Obj[key] extends Type ? key : never;
|
||||
}[keyof Obj];
|
||||
|
||||
/**
|
||||
* Takes a map Map required values and makes all keys matching Keys optional
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
type PartialKeys<
|
||||
Map extends { [name in string]: any },
|
||||
Keys extends keyof Map,
|
||||
> = Partial<Pick<Map, Keys>> & Required<Omit<Map, Keys>>;
|
||||
|
||||
/**
|
||||
* Creates a map of target routes with matching parameters based on a map of external routes.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
type TargetRouteMap<
|
||||
ExternalRoutes extends { [name: string]: ExternalRouteRef },
|
||||
> = {
|
||||
[name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef<
|
||||
infer Params,
|
||||
any
|
||||
>
|
||||
? RouteRef<Params> | SubRouteRef<Params>
|
||||
: never;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that can bind from external routes of a given plugin, to concrete
|
||||
* routes of other plugins. See {@link createApp}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AppRouteBinder = <
|
||||
TExternalRoutes extends { [name: string]: ExternalRouteRef },
|
||||
>(
|
||||
externalRoutes: TExternalRoutes,
|
||||
targetRoutes: PartialKeys<
|
||||
TargetRouteMap<TExternalRoutes>,
|
||||
KeysWithType<TExternalRoutes, ExternalRouteRef<any, true>>
|
||||
>,
|
||||
) => void;
|
||||
|
||||
/** @internal */
|
||||
export function resolveRouteBindings(
|
||||
bindRoutes: ((context: { bind: AppRouteBinder }) => void) | undefined,
|
||||
config: Config,
|
||||
routesById: RouteRefsById,
|
||||
): Map<ExternalRouteRef, RouteRef | SubRouteRef> {
|
||||
const result = new Map<ExternalRouteRef, RouteRef | SubRouteRef>();
|
||||
|
||||
if (bindRoutes) {
|
||||
const bind: AppRouteBinder = (
|
||||
externalRoutes,
|
||||
targetRoutes: { [name: string]: RouteRef | SubRouteRef },
|
||||
) => {
|
||||
for (const [key, value] of Object.entries(targetRoutes)) {
|
||||
const externalRoute = externalRoutes[key];
|
||||
if (!externalRoute) {
|
||||
throw new Error(`Key ${key} is not an existing external route`);
|
||||
}
|
||||
if (!value && !externalRoute.optional) {
|
||||
throw new Error(
|
||||
`External route ${key} is required but was undefined`,
|
||||
);
|
||||
}
|
||||
if (value) {
|
||||
result.set(externalRoute, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
bindRoutes({ bind });
|
||||
}
|
||||
|
||||
const bindingsConfig = config.getOptionalConfig('app.routes.bindings');
|
||||
if (!bindingsConfig) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const bindings = bindingsConfig.get<JsonObject>();
|
||||
for (const [externalRefId, targetRefId] of Object.entries(bindings)) {
|
||||
if (typeof targetRefId !== 'string' || targetRefId === '') {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string`,
|
||||
);
|
||||
}
|
||||
|
||||
const externalRef = routesById.externalRoutes.get(externalRefId);
|
||||
if (!externalRef) {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`,
|
||||
);
|
||||
}
|
||||
// Route bindings defined in config have lower priority than those defined in code
|
||||
if (result.has(externalRef)) {
|
||||
continue;
|
||||
}
|
||||
const targetRef = routesById.routes.get(targetRefId);
|
||||
if (!targetRef) {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`,
|
||||
);
|
||||
}
|
||||
|
||||
result.set(externalRef, targetRef);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -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 {
|
||||
ExternalRouteRef,
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { BackstagePlugin as LegacyBackstagePlugin } from '@backstage/core-plugin-api';
|
||||
|
||||
/** @internal */
|
||||
export type AnyRouteRef = RouteRef | SubRouteRef | ExternalRouteRef;
|
||||
|
||||
/**
|
||||
* A duplicate of the react-router RouteObject, but with routeRef added
|
||||
* @internal
|
||||
*/
|
||||
export interface BackstageRouteObject {
|
||||
caseSensitive: boolean;
|
||||
children?: BackstageRouteObject[];
|
||||
element: React.ReactNode;
|
||||
path: string;
|
||||
routeRefs: Set<RouteRef>;
|
||||
plugins: Set<LegacyBackstagePlugin>;
|
||||
}
|
||||
@@ -19,13 +19,13 @@ import {
|
||||
createExtensionOverrides,
|
||||
createPageExtension,
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
createThemeExtension,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createApp, createInstances } from './createApp';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
|
||||
import React from 'react';
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
const extBaseConfig = {
|
||||
id: 'test',
|
||||
@@ -83,14 +83,14 @@ describe('createInstances', () => {
|
||||
const ExtensionA = createPageExtension({
|
||||
id: 'A',
|
||||
defaultPath: '/',
|
||||
routeRef: createRouteRef({ id: 'A.route' }),
|
||||
routeRef: createRouteRef(),
|
||||
loader: async () => <div>Extension A</div>,
|
||||
});
|
||||
|
||||
const ExtensionB = createPageExtension({
|
||||
id: 'B',
|
||||
defaultPath: '/',
|
||||
routeRef: createRouteRef({ id: 'B.route' }),
|
||||
routeRef: createRouteRef(),
|
||||
loader: async () => <div>Extension B</div>,
|
||||
});
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
coreExtensionData,
|
||||
ExtensionDataRef,
|
||||
ExtensionOverrides,
|
||||
RouteRef,
|
||||
useRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { Core } from '../extensions/Core';
|
||||
import { CoreRoutes } from '../extensions/CoreRoutes';
|
||||
@@ -44,11 +46,9 @@ import {
|
||||
ConfigApi,
|
||||
configApiRef,
|
||||
IconComponent,
|
||||
RouteRef,
|
||||
BackstagePlugin as LegacyBackstagePlugin,
|
||||
featureFlagsApiRef,
|
||||
attachComponentData,
|
||||
useRouteRef,
|
||||
identityApiRef,
|
||||
AppTheme,
|
||||
} from '@backstage/core-plugin-api';
|
||||
@@ -57,7 +57,6 @@ import {
|
||||
ApiFactoryRegistry,
|
||||
ApiProvider,
|
||||
ApiResolver,
|
||||
AppRouteBinder,
|
||||
AppThemeSelector,
|
||||
} from '@backstage/core-app-api';
|
||||
|
||||
@@ -75,10 +74,6 @@ import { defaultConfigLoaderSync } from '../../../core-app-api/src/app/defaultCo
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { overrideBaseUrlConfigs } from '../../../core-app-api/src/app/overrideBaseUrlConfigs';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { RoutingProvider } from '../../../core-app-api/src/routing/RoutingProvider';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { resolveRouteBindings } from '../../../core-app-api/src/app/resolveRouteBindings';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
|
||||
@@ -97,6 +92,10 @@ import {
|
||||
appLanguageApiRef,
|
||||
translationApiRef,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
import { AppRouteBinder } from '../routing';
|
||||
import { RoutingProvider } from '../routing/RoutingProvider';
|
||||
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
|
||||
import { collectRouteIds } from '../routing/collectRouteIds';
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionTreeNode {
|
||||
@@ -307,13 +306,19 @@ export function createApp(options: {
|
||||
),
|
||||
);
|
||||
|
||||
const routeIds = collectRouteIds(allFeatures);
|
||||
|
||||
const App = () => (
|
||||
<ApiProvider apis={createApiHolder(coreInstance, config)}>
|
||||
<AppContextProvider appContext={appContext}>
|
||||
<AppThemeProvider>
|
||||
<RoutingProvider
|
||||
{...extractRouteInfoFromInstanceTree(coreInstance)}
|
||||
routeBindings={resolveRouteBindings(options.bindRoutes)}
|
||||
routeBindings={resolveRouteBindings(
|
||||
options.bindRoutes,
|
||||
config,
|
||||
routeIds,
|
||||
)}
|
||||
>
|
||||
{/* TODO: set base path using the logic from AppRouter */}
|
||||
<BrowserRouter>
|
||||
|
||||
@@ -7,15 +7,12 @@
|
||||
|
||||
import { AnyApiFactory } from '@backstage/core-plugin-api';
|
||||
import { AnyApiRef } from '@backstage/core-plugin-api';
|
||||
import { AnyExternalRoutes } from '@backstage/core-plugin-api';
|
||||
import { AnyRoutes } from '@backstage/core-plugin-api';
|
||||
import { AppTheme } from '@backstage/core-plugin-api';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
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';
|
||||
@@ -41,6 +38,23 @@ export type AnyExtensionInputMap = {
|
||||
>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type AnyExternalRoutes = {
|
||||
[name in string]: ExternalRouteRef;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AnyRouteRefParams =
|
||||
| {
|
||||
[param in string]: string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
// @public (undocumented)
|
||||
export type AnyRoutes = {
|
||||
[name in string]: RouteRef;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface BackstagePlugin<
|
||||
Routes extends AnyRoutes = AnyRoutes,
|
||||
@@ -79,7 +93,7 @@ export const coreExtensionData: {
|
||||
reactElement: ConfigurableExtensionDataRef<JSX_2.Element, {}>;
|
||||
routePath: ConfigurableExtensionDataRef<string, {}>;
|
||||
apiFactory: ConfigurableExtensionDataRef<AnyApiFactory, {}>;
|
||||
routeRef: ConfigurableExtensionDataRef<RouteRef, {}>;
|
||||
routeRef: ConfigurableExtensionDataRef<RouteRef<AnyRouteRefParams>, {}>;
|
||||
navTarget: ConfigurableExtensionDataRef<NavTarget, {}>;
|
||||
theme: ConfigurableExtensionDataRef<AppTheme, {}>;
|
||||
};
|
||||
@@ -173,10 +187,35 @@ export function createExtensionOverrides(
|
||||
options: ExtensionOverridesOptions,
|
||||
): ExtensionOverrides;
|
||||
|
||||
// @public
|
||||
export function createExternalRouteRef<
|
||||
TParams extends
|
||||
| {
|
||||
[param in TParamKeys]: string;
|
||||
}
|
||||
| undefined = undefined,
|
||||
TOptional extends boolean = false,
|
||||
TParamKeys extends string = string,
|
||||
>(options?: {
|
||||
readonly params?: string extends TParamKeys
|
||||
? (keyof TParams)[]
|
||||
: TParamKeys[];
|
||||
optional?: TOptional;
|
||||
}): ExternalRouteRef<
|
||||
keyof TParams extends never
|
||||
? undefined
|
||||
: string extends TParamKeys
|
||||
? TParams
|
||||
: {
|
||||
[param in TParamKeys]: string;
|
||||
},
|
||||
TOptional
|
||||
>;
|
||||
|
||||
// @public
|
||||
export function createNavItemExtension(options: {
|
||||
id: string;
|
||||
routeRef: RouteRef;
|
||||
routeRef: RouteRef<undefined>;
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
}): Extension<{
|
||||
@@ -215,17 +254,46 @@ export function createPageExtension<
|
||||
|
||||
// @public (undocumented)
|
||||
export function createPlugin<
|
||||
Routes extends AnyRoutes = AnyRoutes,
|
||||
ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes,
|
||||
Routes extends AnyRoutes = {},
|
||||
ExternalRoutes extends AnyExternalRoutes = {},
|
||||
>(
|
||||
options: PluginOptions<Routes, ExternalRoutes>,
|
||||
): BackstagePlugin<Routes, ExternalRoutes>;
|
||||
|
||||
// @public
|
||||
export function createRouteRef<
|
||||
TParams extends
|
||||
| {
|
||||
[param in TParamKeys]: string;
|
||||
}
|
||||
| undefined = undefined,
|
||||
TParamKeys extends string = string,
|
||||
>(config?: {
|
||||
readonly params: string extends TParamKeys ? (keyof TParams)[] : TParamKeys[];
|
||||
}): RouteRef<
|
||||
keyof TParams extends never
|
||||
? undefined
|
||||
: string extends TParamKeys
|
||||
? TParams
|
||||
: {
|
||||
[param in TParamKeys]: string;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createSchemaFromZod<TOutput, TInput>(
|
||||
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
|
||||
): PortableSchema<TOutput>;
|
||||
|
||||
// @public
|
||||
export function createSubRouteRef<
|
||||
Path extends string,
|
||||
ParentParams extends AnyRouteRefParams = never,
|
||||
>(config: {
|
||||
path: Path;
|
||||
parent: RouteRef<ParentParams>;
|
||||
}): MakeSubRouteRef<PathParams<Path>, ParentParams>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createThemeExtension(theme: AppTheme): Extension<never>;
|
||||
|
||||
@@ -344,11 +412,24 @@ export interface ExtensionOverridesOptions {
|
||||
extensions: Extension<unknown>[];
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface ExternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
TOptional extends boolean = boolean,
|
||||
> {
|
||||
// (undocumented)
|
||||
readonly $$type: '@backstage/ExternalRouteRef';
|
||||
// (undocumented)
|
||||
readonly optional: TOptional;
|
||||
// (undocumented)
|
||||
readonly T: TParams;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type NavTarget = {
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
routeRef: RouteRef<{}>;
|
||||
routeRef: RouteRef<undefined>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -371,4 +452,51 @@ export type PortableSchema<TOutput> = {
|
||||
parse: (input: unknown) => TOutput;
|
||||
schema: JsonObject;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type RouteFunc<TParams extends AnyRouteRefParams> = (
|
||||
...[params]: TParams extends undefined
|
||||
? readonly []
|
||||
: readonly [params: TParams]
|
||||
) => string;
|
||||
|
||||
// @public
|
||||
export interface RouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
> {
|
||||
// (undocumented)
|
||||
readonly $$type: '@backstage/RouteRef';
|
||||
// (undocumented)
|
||||
readonly T: TParams;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SubRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
> {
|
||||
// (undocumented)
|
||||
readonly $$type: '@backstage/SubRouteRef';
|
||||
// (undocumented)
|
||||
readonly path: string;
|
||||
// (undocumented)
|
||||
readonly T: TParams;
|
||||
}
|
||||
|
||||
// @public
|
||||
export function useRouteRef<
|
||||
TOptional extends boolean,
|
||||
TParams extends AnyRouteRefParams,
|
||||
>(
|
||||
routeRef: ExternalRouteRef<TParams, TOptional>,
|
||||
): TParams extends true ? RouteFunc<TParams> | undefined : RouteFunc<TParams>;
|
||||
|
||||
// @public
|
||||
export function useRouteRef<TParams extends AnyRouteRefParams>(
|
||||
routeRef: RouteRef<TParams> | SubRouteRef<TParams>,
|
||||
): RouteFunc<TParams>;
|
||||
|
||||
// @public
|
||||
export function useRouteRefParams<Params extends AnyRouteRefParams>(
|
||||
_routeRef: RouteRef<Params> | SubRouteRef<Params>,
|
||||
): Params;
|
||||
```
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
"@backstage/frontend-app-api": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^12.1.3"
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"history": "^5.3.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -39,6 +41,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"zod": "^3.21.4",
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
createExtension,
|
||||
coreExtensionData,
|
||||
} from '../wiring';
|
||||
import { AnyExtensionInputMap, Expand } from '../wiring/createExtension';
|
||||
import { AnyExtensionInputMap } from '../wiring/createExtension';
|
||||
import { Expand } from '../types';
|
||||
|
||||
/** @public */
|
||||
export function createApiExtension<
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IconComponent, RouteRef } from '@backstage/core-plugin-api';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
|
||||
import { coreExtensionData, createExtension } from '../wiring';
|
||||
import { RouteRef } from '../routing';
|
||||
|
||||
/**
|
||||
* Helper for creating extensions for a nav item.
|
||||
@@ -24,7 +25,7 @@ import { coreExtensionData, createExtension } from '../wiring';
|
||||
*/
|
||||
export function createNavItemExtension(options: {
|
||||
id: string;
|
||||
routeRef: RouteRef;
|
||||
routeRef: RouteRef<undefined>;
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
}) {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import React from 'react';
|
||||
import { ExtensionBoundary } from '../components';
|
||||
import { createSchemaFromZod, PortableSchema } from '../schema';
|
||||
@@ -24,7 +23,9 @@ import {
|
||||
Extension,
|
||||
ExtensionInputValues,
|
||||
} from '../wiring';
|
||||
import { AnyExtensionInputMap, Expand } from '../wiring/createExtension';
|
||||
import { AnyExtensionInputMap } from '../wiring/createExtension';
|
||||
import { Expand } from '../types';
|
||||
import { RouteRef } from '../routing';
|
||||
|
||||
/**
|
||||
* Helper for creating extensions for a routable React page component.
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
|
||||
export * from './components';
|
||||
export * from './extensions';
|
||||
export * from './routing';
|
||||
export * from './schema';
|
||||
export * from './wiring';
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2020 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 {
|
||||
ExternalRouteRef,
|
||||
createExternalRouteRef,
|
||||
toInternalExternalRouteRef,
|
||||
} from './ExternalRouteRef';
|
||||
import { AnyRouteRefParams } from './types';
|
||||
|
||||
describe('ExternalRouteRef', () => {
|
||||
it('should be created', () => {
|
||||
const routeRef: ExternalRouteRef<undefined> = createExternalRouteRef();
|
||||
const internal = toInternalExternalRouteRef(routeRef);
|
||||
expect(internal.getParams()).toEqual([]);
|
||||
expect(internal.optional).toBe(false);
|
||||
|
||||
expect(String(internal)).toMatch(
|
||||
/^ExternalRouteRef\{created at '.*ExternalRouteRef\.test\.ts.*'\}$/,
|
||||
);
|
||||
internal.setId('some-id');
|
||||
expect(String(internal)).toBe('ExternalRouteRef{some-id}');
|
||||
});
|
||||
|
||||
it('should be created as optional', () => {
|
||||
const routeRef: ExternalRouteRef<undefined, true> = createExternalRouteRef({
|
||||
params: [],
|
||||
optional: true,
|
||||
});
|
||||
const internal = toInternalExternalRouteRef(routeRef);
|
||||
expect(internal.getParams()).toEqual([]);
|
||||
expect(internal.optional).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be created with params', () => {
|
||||
const routeRef: ExternalRouteRef<{
|
||||
x: string;
|
||||
y: string;
|
||||
}> = createExternalRouteRef({ params: ['x', 'y'] });
|
||||
const internal = toInternalExternalRouteRef(routeRef);
|
||||
expect(internal.getParams()).toEqual(['x', 'y']);
|
||||
expect(internal.optional).toEqual(false);
|
||||
});
|
||||
|
||||
it('should be created as optional with params', () => {
|
||||
const routeRef: ExternalRouteRef<{
|
||||
x: string;
|
||||
y: string;
|
||||
}> = createExternalRouteRef({ params: ['x', 'y'], optional: true });
|
||||
const internal = toInternalExternalRouteRef(routeRef);
|
||||
expect(internal.getParams()).toEqual(['x', 'y']);
|
||||
expect(internal.optional).toEqual(true);
|
||||
});
|
||||
|
||||
it('should properly infer and validate parameter types and assignments', () => {
|
||||
function checkRouteRef<
|
||||
T extends AnyRouteRefParams,
|
||||
TOptional extends boolean,
|
||||
TCheck extends TOptional,
|
||||
>(
|
||||
_ref: ExternalRouteRef<T, TOptional>,
|
||||
_params: T extends undefined ? undefined : T,
|
||||
_optional: TCheck,
|
||||
) {}
|
||||
|
||||
const _1 = createExternalRouteRef({ params: ['notX'] });
|
||||
checkRouteRef(_1, { notX: '' }, false);
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_1, { x: '' }, false);
|
||||
|
||||
const _2 = createExternalRouteRef({ params: ['x'], optional: true });
|
||||
checkRouteRef(_2, { x: '' }, true);
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_2, undefined, false);
|
||||
|
||||
const _3 = createExternalRouteRef({ params: ['x', 'y'] });
|
||||
checkRouteRef(_3, { x: '', y: '' }, false);
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_3, { x: '' }, false);
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_3, { x: '', y: '', z: '' }, false);
|
||||
|
||||
const _4 = createExternalRouteRef({ params: [] });
|
||||
checkRouteRef(_4, undefined, false);
|
||||
// @ts-expect-error
|
||||
checkRouteRef<any>(_4, { x: '' });
|
||||
|
||||
const _5 = createExternalRouteRef();
|
||||
checkRouteRef(_5, undefined, false);
|
||||
// @ts-expect-error
|
||||
checkRouteRef<any>(_5, { x: '' });
|
||||
|
||||
const _6 = createExternalRouteRef({ optional: true });
|
||||
checkRouteRef(_6, undefined, true);
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_6, undefined, false);
|
||||
|
||||
// To avoid complains about missing expectations and unused vars
|
||||
expect([_1, _2, _3, _4, _5, _6].join('')).toEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2020 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 { RouteRefImpl } from './RouteRef';
|
||||
import { describeParentCallSite } from './describeParentCallSite';
|
||||
import { AnyRouteRefParams } from './types';
|
||||
|
||||
/**
|
||||
* Route descriptor, to be later bound to a concrete route by the app. Used to implement cross-plugin route references.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://backstage.io/docs/plugins/composability#routing-system}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ExternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
TOptional extends boolean = boolean,
|
||||
> {
|
||||
readonly $$type: '@backstage/ExternalRouteRef';
|
||||
readonly T: TParams;
|
||||
readonly optional: TOptional;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalExternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
TOptional extends boolean = boolean,
|
||||
> extends ExternalRouteRef<TParams, TOptional> {
|
||||
readonly version: 'v1';
|
||||
getParams(): string[];
|
||||
getDescription(): string;
|
||||
|
||||
setId(id: string): void;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalExternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
TOptional extends boolean = boolean,
|
||||
>(
|
||||
resource: ExternalRouteRef<TParams, TOptional>,
|
||||
): InternalExternalRouteRef<TParams, TOptional> {
|
||||
const r = resource as InternalExternalRouteRef<TParams, TOptional>;
|
||||
if (r.$$type !== '@backstage/ExternalRouteRef') {
|
||||
throw new Error(`Invalid ExternalRouteRef, bad type '${r.$$type}'`);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isExternalRouteRef(opaque: {
|
||||
$$type: string;
|
||||
}): opaque is ExternalRouteRef {
|
||||
return opaque.$$type === '@backstage/ExternalRouteRef';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
class ExternalRouteRefImpl
|
||||
extends RouteRefImpl
|
||||
implements InternalExternalRouteRef
|
||||
{
|
||||
readonly $$type = '@backstage/ExternalRouteRef' as any;
|
||||
|
||||
constructor(
|
||||
readonly optional: boolean,
|
||||
readonly params: string[] = [],
|
||||
creationSite: string,
|
||||
) {
|
||||
super(params, creationSite);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a route descriptor, to be later bound to a concrete route by the app. Used to implement cross-plugin route references.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://backstage.io/docs/plugins/composability#routing-system}.
|
||||
*
|
||||
* @param options - Description of the route reference to be created.
|
||||
* @public
|
||||
*/
|
||||
export function createExternalRouteRef<
|
||||
TParams extends { [param in TParamKeys]: string } | undefined = undefined,
|
||||
TOptional extends boolean = false,
|
||||
TParamKeys extends string = string,
|
||||
>(options?: {
|
||||
/**
|
||||
* The parameters that will be provided to the external route reference.
|
||||
*/
|
||||
readonly params?: string extends TParamKeys
|
||||
? (keyof TParams)[]
|
||||
: TParamKeys[];
|
||||
|
||||
/**
|
||||
* Whether or not this route is optional, defaults to false.
|
||||
*
|
||||
* Optional external routes are not required to be bound in the app, and
|
||||
* if they aren't, `useExternalRouteRef` will return `undefined`.
|
||||
*/
|
||||
optional?: TOptional;
|
||||
}): ExternalRouteRef<
|
||||
keyof TParams extends never
|
||||
? undefined
|
||||
: string extends TParamKeys
|
||||
? TParams
|
||||
: { [param in TParamKeys]: string },
|
||||
TOptional
|
||||
> {
|
||||
return new ExternalRouteRefImpl(
|
||||
Boolean(options?.optional),
|
||||
options?.params as string[] | undefined,
|
||||
describeParentCallSite(),
|
||||
) as ExternalRouteRef<any, any>;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2020 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 { AnyRouteRefParams } from './types';
|
||||
import { RouteRef, createRouteRef, toInternalRouteRef } from './RouteRef';
|
||||
|
||||
describe('RouteRef', () => {
|
||||
it('should be created and have a mutable ID', () => {
|
||||
const routeRef: RouteRef<undefined> = createRouteRef();
|
||||
const internal = toInternalRouteRef(routeRef);
|
||||
expect(internal.T).toBe(undefined);
|
||||
expect(internal.getParams()).toEqual([]);
|
||||
expect(internal.getDescription()).toMatch(/RouteRef\.test\.ts/);
|
||||
|
||||
expect(String(internal)).toMatch(
|
||||
/^RouteRef\{created at .*RouteRef\.test\.ts.*\}$/,
|
||||
);
|
||||
|
||||
expect(() => internal.setId('')).toThrow(
|
||||
'RouteRef id must be a non-empty string',
|
||||
);
|
||||
|
||||
internal.setId('some-id');
|
||||
expect(String(internal)).toBe('RouteRef{some-id}');
|
||||
|
||||
expect(() => internal.setId('some-other-id')).toThrow(
|
||||
"RouteRef was referenced twice as both 'some-id' and 'some-other-id'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should be created with params', () => {
|
||||
const routeRef: RouteRef<{
|
||||
x: string;
|
||||
y: string;
|
||||
}> = createRouteRef({
|
||||
params: ['x', 'y'],
|
||||
});
|
||||
const internal = toInternalRouteRef(routeRef);
|
||||
expect(internal.getParams()).toEqual(['x', 'y']);
|
||||
expect(internal.getDescription()).toMatch(/RouteRef\.test\.ts/);
|
||||
});
|
||||
|
||||
it('should properly infer and validate parameter types and assignments', () => {
|
||||
function checkRouteRef<T extends AnyRouteRefParams>(
|
||||
_ref: RouteRef<T>,
|
||||
_params: T extends undefined ? undefined : T,
|
||||
) {}
|
||||
|
||||
const _1 = createRouteRef({ params: ['x'] });
|
||||
checkRouteRef(_1, { x: '' });
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_1, { y: '' });
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_1, undefined);
|
||||
|
||||
const _2 = createRouteRef({ params: ['x', 'y'] });
|
||||
checkRouteRef(_2, { x: '', y: '' });
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_2, { x: '' });
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_2, undefined);
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_2, { x: '', z: '' });
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_2, { x: '', y: '', z: '' });
|
||||
|
||||
const _3 = createRouteRef({ params: [] });
|
||||
checkRouteRef(_3, undefined);
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_3, { x: '' });
|
||||
|
||||
const _4 = createRouteRef();
|
||||
checkRouteRef(_4, undefined);
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_4, { x: '' });
|
||||
|
||||
// To avoid complains about missing expectations and unused vars
|
||||
expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2020 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 { describeParentCallSite } from './describeParentCallSite';
|
||||
import { AnyRouteRefParams } from './types';
|
||||
|
||||
/**
|
||||
* Absolute route reference.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://backstage.io/docs/plugins/composability#routing-system}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface RouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
> {
|
||||
readonly $$type: '@backstage/RouteRef';
|
||||
readonly T: TParams;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
> extends RouteRef<TParams> {
|
||||
readonly version: 'v1';
|
||||
getParams(): string[];
|
||||
getDescription(): string;
|
||||
|
||||
setId(id: string): void;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
>(resource: RouteRef<TParams>): InternalRouteRef<TParams> {
|
||||
const r = resource as InternalRouteRef<TParams>;
|
||||
if (r.$$type !== '@backstage/RouteRef') {
|
||||
throw new Error(`Invalid RouteRef, bad type '${r.$$type}'`);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isRouteRef(opaque: { $$type: string }): opaque is RouteRef {
|
||||
return opaque.$$type === '@backstage/RouteRef';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class RouteRefImpl implements InternalRouteRef {
|
||||
readonly $$type = '@backstage/RouteRef';
|
||||
readonly version = 'v1';
|
||||
declare readonly T: never;
|
||||
|
||||
#id?: string;
|
||||
#params: string[];
|
||||
#creationSite: string;
|
||||
|
||||
constructor(readonly params: string[] = [], creationSite: string) {
|
||||
this.#params = params;
|
||||
this.#creationSite = creationSite;
|
||||
}
|
||||
|
||||
getParams(): string[] {
|
||||
return this.#params;
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
if (this.#id) {
|
||||
return this.#id;
|
||||
}
|
||||
return `created at '${this.#creationSite}'`;
|
||||
}
|
||||
|
||||
get #name() {
|
||||
return this.$$type.slice('@backstage/'.length);
|
||||
}
|
||||
|
||||
setId(id: string): void {
|
||||
if (!id) {
|
||||
throw new Error(`${this.#name} id must be a non-empty string`);
|
||||
}
|
||||
if (this.#id) {
|
||||
throw new Error(
|
||||
`${this.#name} was referenced twice as both '${this.#id}' and '${id}'`,
|
||||
);
|
||||
}
|
||||
this.#id = id;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.#name}{${this.getDescription()}}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RouteRef} from a route descriptor.
|
||||
*
|
||||
* @param config - Description of the route reference to be created.
|
||||
* @public
|
||||
*/
|
||||
export function createRouteRef<
|
||||
// Params is the type that we care about and the one to be embedded in the route ref.
|
||||
// For example, given the params ['name', 'kind'], Params will be {name: string, kind: string}
|
||||
TParams extends { [param in TParamKeys]: string } | undefined = undefined,
|
||||
TParamKeys extends string = string,
|
||||
>(config?: {
|
||||
/** A list of parameter names that the path that this route ref is bound to must contain */
|
||||
readonly params: string extends TParamKeys ? (keyof TParams)[] : TParamKeys[];
|
||||
}): RouteRef<
|
||||
keyof TParams extends never
|
||||
? undefined
|
||||
: string extends TParamKeys
|
||||
? TParams
|
||||
: { [param in TParamKeys]: string }
|
||||
> {
|
||||
return new RouteRefImpl(
|
||||
config?.params as string[] | undefined,
|
||||
describeParentCallSite(),
|
||||
) as RouteRef<any>;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2020 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 { AnyRouteRefParams } from './types';
|
||||
import {
|
||||
SubRouteRef,
|
||||
createSubRouteRef,
|
||||
toInternalSubRouteRef,
|
||||
} from './SubRouteRef';
|
||||
import { createRouteRef, toInternalRouteRef } from './RouteRef';
|
||||
|
||||
const parent = createRouteRef();
|
||||
const parentX = createRouteRef({ params: ['x'] });
|
||||
|
||||
describe('SubRouteRef', () => {
|
||||
it('should be created', () => {
|
||||
const internalParent = toInternalRouteRef(createRouteRef());
|
||||
const routeRef: SubRouteRef = createSubRouteRef({
|
||||
parent: internalParent,
|
||||
path: '/foo',
|
||||
});
|
||||
const internal = toInternalSubRouteRef(routeRef);
|
||||
expect(internal.path).toBe('/foo');
|
||||
expect(internal.T).toBe(undefined);
|
||||
expect(internal.getParent()).toBe(internalParent);
|
||||
expect(internal.getParams()).toEqual([]);
|
||||
expect(String(internal)).toMatch(
|
||||
/SubRouteRef\{at \/foo with parent created at '.*SubRouteRef\.test\.ts.*'\}/,
|
||||
);
|
||||
internalParent.setId('some-id');
|
||||
expect(String(internal)).toBe('SubRouteRef{at /foo with parent some-id}');
|
||||
});
|
||||
|
||||
it('should be created with params', () => {
|
||||
const routeRef: SubRouteRef<{ bar: string }> = createSubRouteRef({
|
||||
parent,
|
||||
path: '/foo/:bar',
|
||||
});
|
||||
const internal = toInternalSubRouteRef(routeRef);
|
||||
expect(internal.path).toBe('/foo/:bar');
|
||||
expect(internal.getParent()).toBe(parent);
|
||||
expect(internal.getParams()).toEqual(['bar']);
|
||||
});
|
||||
|
||||
it('should be created with merged params', () => {
|
||||
const routeRef: SubRouteRef<{
|
||||
x: string;
|
||||
y: string;
|
||||
z: string;
|
||||
}> = createSubRouteRef({
|
||||
parent: parentX,
|
||||
path: '/foo/:y/:z',
|
||||
});
|
||||
const internal = toInternalSubRouteRef(routeRef);
|
||||
expect(internal.path).toBe('/foo/:y/:z');
|
||||
expect(internal.getParent()).toBe(parentX);
|
||||
expect(internal.getParams()).toEqual(['x', 'y', 'z']);
|
||||
});
|
||||
|
||||
it('should be created with params from parent', () => {
|
||||
const routeRef: SubRouteRef<{ x: string }> = createSubRouteRef({
|
||||
parent: parentX,
|
||||
path: '/foo/bar',
|
||||
});
|
||||
const internal = toInternalSubRouteRef(routeRef);
|
||||
expect(internal.path).toBe('/foo/bar');
|
||||
expect(internal.getParent()).toBe(parentX);
|
||||
expect(internal.getParams()).toEqual(['x']);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['foo', "SubRouteRef path must start with '/', got 'foo'"],
|
||||
[':foo', "SubRouteRef path must start with '/', got ':foo'"],
|
||||
['', "SubRouteRef path must start with '/', got ''"],
|
||||
['/', "SubRouteRef path must not end with '/', got '/'"],
|
||||
['/foo/', "SubRouteRef path must not end with '/', got '/foo/'"],
|
||||
['/foo/:x', 'SubRouteRef may not have params that overlap with its parent'],
|
||||
['/:/foo', "SubRouteRef path has invalid param, got ''"],
|
||||
['/:inva:lid/foo', "SubRouteRef path has invalid param, got 'inva:lid'"],
|
||||
['/:inva=lid/foo', "SubRouteRef path has invalid param, got 'inva=lid'"],
|
||||
])('should throw if path is invalid, %s', (path, message) => {
|
||||
expect(() => createSubRouteRef({ path, parent: parentX })).toThrow(message);
|
||||
});
|
||||
|
||||
it('should properly infer and parse path parameters', () => {
|
||||
function checkSubRouteRef<T extends AnyRouteRefParams>(
|
||||
_ref: SubRouteRef<T>,
|
||||
_params: T extends undefined ? undefined : T,
|
||||
) {}
|
||||
|
||||
const _1 = createSubRouteRef({ parent, path: '/foo/bar' });
|
||||
// @ts-expect-error
|
||||
checkSubRouteRef(_1, { x: '' });
|
||||
checkSubRouteRef(_1, undefined);
|
||||
|
||||
const _2 = createSubRouteRef({ parent, path: '/foo/:x/:y' });
|
||||
// @ts-expect-error
|
||||
checkSubRouteRef(_2, undefined);
|
||||
// @ts-expect-error
|
||||
checkSubRouteRef(_2, { x: '', z: '' });
|
||||
// @ts-expect-error
|
||||
checkSubRouteRef(_2, { y: '' });
|
||||
// @ts-expect-error
|
||||
checkSubRouteRef(_2, { x: '', y: '', z: '' });
|
||||
checkSubRouteRef(_2, { x: '', y: '' });
|
||||
|
||||
const _3 = createSubRouteRef({ parent: parentX, path: '/foo' });
|
||||
// @ts-expect-error
|
||||
checkSubRouteRef(_3, undefined);
|
||||
// @ts-expect-error
|
||||
checkSubRouteRef(_3, { y: '' });
|
||||
checkSubRouteRef(_3, { x: '' });
|
||||
|
||||
const _4 = createSubRouteRef({ parent: parentX, path: '/foo/:y' });
|
||||
// @ts-expect-error
|
||||
checkSubRouteRef(_4, undefined);
|
||||
// @ts-expect-error
|
||||
checkSubRouteRef(_4, { x: '', z: '' });
|
||||
// @ts-expect-error
|
||||
checkSubRouteRef(_4, { y: '' });
|
||||
checkSubRouteRef(_4, { x: '', y: '' });
|
||||
|
||||
// To avoid complains about missing expectations and unused vars
|
||||
expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright 2020 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, toInternalRouteRef } from './RouteRef';
|
||||
import { AnyRouteRefParams } from './types';
|
||||
|
||||
// Should match the pattern in react-router
|
||||
const PARAM_PATTERN = /^\w+$/;
|
||||
|
||||
/**
|
||||
* Descriptor of a route relative to an absolute {@link RouteRef}.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://backstage.io/docs/plugins/composability#routing-system}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SubRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
> {
|
||||
readonly $$type: '@backstage/SubRouteRef';
|
||||
|
||||
readonly T: TParams;
|
||||
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalSubRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
> extends SubRouteRef<TParams> {
|
||||
readonly version: 'v1';
|
||||
|
||||
getParams(): string[];
|
||||
getParent(): RouteRef;
|
||||
getDescription(): string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalSubRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
>(resource: SubRouteRef<TParams>): InternalSubRouteRef<TParams> {
|
||||
const r = resource as InternalSubRouteRef<TParams>;
|
||||
if (r.$$type !== '@backstage/SubRouteRef') {
|
||||
throw new Error(`Invalid SubRouteRef, bad type '${r.$$type}'`);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isSubRouteRef(opaque: {
|
||||
$$type: string;
|
||||
}): opaque is SubRouteRef {
|
||||
return opaque.$$type === '@backstage/SubRouteRef';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class SubRouteRefImpl<TParams extends AnyRouteRefParams>
|
||||
implements SubRouteRef<TParams>
|
||||
{
|
||||
readonly $$type = '@backstage/SubRouteRef';
|
||||
readonly version = 'v1';
|
||||
declare readonly T: never;
|
||||
|
||||
#params: string[];
|
||||
#parent: RouteRef;
|
||||
|
||||
constructor(readonly path: string, params: string[], parent: RouteRef) {
|
||||
this.#params = params;
|
||||
this.#parent = parent;
|
||||
}
|
||||
|
||||
getParams(): string[] {
|
||||
return this.#params;
|
||||
}
|
||||
|
||||
getParent(): RouteRef {
|
||||
return this.#parent;
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const parent = toInternalRouteRef(this.#parent);
|
||||
return `at ${this.path} with parent ${parent.getDescription()}`;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `SubRouteRef{${this.getDescription()}}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in {@link PathParams} type declaration.
|
||||
* @ignore
|
||||
*/
|
||||
type ParamPart<S extends string> = S extends `:${infer Param}` ? Param : never;
|
||||
|
||||
/**
|
||||
* Used in {@link PathParams} type declaration.
|
||||
* @ignore
|
||||
*/
|
||||
type ParamNames<S extends string> = S extends `${infer Part}/${infer Rest}`
|
||||
? ParamPart<Part> | ParamNames<Rest>
|
||||
: ParamPart<S>;
|
||||
/**
|
||||
* This utility type helps us infer a Param object type from a string path
|
||||
* For example, `/foo/:bar/:baz` inferred to `{ bar: string, baz: string }`
|
||||
* @ignore
|
||||
*/
|
||||
type PathParams<S extends string> = { [name in ParamNames<S>]: string };
|
||||
|
||||
/**
|
||||
* Merges a param object type with an optional params type into a params object.
|
||||
* @ignore
|
||||
*/
|
||||
type MergeParams<
|
||||
P1 extends { [param in string]: string },
|
||||
P2 extends AnyRouteRefParams,
|
||||
> = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2);
|
||||
|
||||
/**
|
||||
* Convert empty params to undefined.
|
||||
* @ignore
|
||||
*/
|
||||
type TrimEmptyParams<Params extends { [param in string]: string }> =
|
||||
keyof Params extends never ? undefined : Params;
|
||||
|
||||
/**
|
||||
* Creates a SubRouteRef type given the desired parameters and parent route parameters.
|
||||
* The parameters types are merged together while ensuring that there is no overlap between the two.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
type MakeSubRouteRef<
|
||||
Params extends { [param in string]: string },
|
||||
ParentParams extends AnyRouteRefParams,
|
||||
> = keyof Params & keyof ParentParams extends never
|
||||
? SubRouteRef<TrimEmptyParams<MergeParams<Params, ParentParams>>>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Create a {@link SubRouteRef} from a route descriptor.
|
||||
*
|
||||
* @param config - Description of the route reference to be created.
|
||||
* @public
|
||||
*/
|
||||
export function createSubRouteRef<
|
||||
Path extends string,
|
||||
ParentParams extends AnyRouteRefParams = never,
|
||||
>(config: {
|
||||
path: Path;
|
||||
parent: RouteRef<ParentParams>;
|
||||
}): MakeSubRouteRef<PathParams<Path>, ParentParams> {
|
||||
const { path, parent } = config;
|
||||
type Params = PathParams<Path>;
|
||||
|
||||
const internalParent = toInternalRouteRef(parent);
|
||||
const parentParams = internalParent.getParams();
|
||||
|
||||
// Collect runtime parameters from the path, e.g. ['bar', 'baz'] from '/foo/:bar/:baz'
|
||||
const pathParams = path
|
||||
.split('/')
|
||||
.filter(p => p.startsWith(':'))
|
||||
.map(p => p.substring(1));
|
||||
const params = [...parentParams, ...pathParams];
|
||||
|
||||
if (parentParams.some(p => pathParams.includes(p as string))) {
|
||||
throw new Error(
|
||||
'SubRouteRef may not have params that overlap with its parent',
|
||||
);
|
||||
}
|
||||
if (!path.startsWith('/')) {
|
||||
throw new Error(`SubRouteRef path must start with '/', got '${path}'`);
|
||||
}
|
||||
if (path.endsWith('/')) {
|
||||
throw new Error(`SubRouteRef path must not end with '/', got '${path}'`);
|
||||
}
|
||||
for (const param of pathParams) {
|
||||
if (!PARAM_PATTERN.test(param)) {
|
||||
throw new Error(`SubRouteRef path has invalid param, got '${param}'`);
|
||||
}
|
||||
}
|
||||
|
||||
// We ensure that the type of the return type is sane here
|
||||
const subRouteRef = new SubRouteRefImpl(
|
||||
path,
|
||||
params as string[],
|
||||
parent,
|
||||
) as SubRouteRef<TrimEmptyParams<MergeParams<Params, ParentParams>>>;
|
||||
|
||||
// But skip type checking of the return value itself, because the conditional
|
||||
// type checking of the parent parameter overlap is tricky to express.
|
||||
return subRouteRef as any;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 { describeParentCallSite } from './describeParentCallSite';
|
||||
|
||||
class ChromeError {
|
||||
name = 'Error';
|
||||
message = 'eHgtF5hmbrXyiEvo';
|
||||
get stack() {
|
||||
return `Error: eHgtF5hmbrXyiEvo
|
||||
at describeParentCallSite (describeParentCallSite.js:1:11)
|
||||
at parent (parent.js:2:22)
|
||||
at caller (parent-caller.js:3:33)
|
||||
at other.js:3:33`;
|
||||
}
|
||||
}
|
||||
|
||||
class SafariError {
|
||||
name = 'Error';
|
||||
message = 'eHgtF5hmbrXyiEvo';
|
||||
get stack() {
|
||||
return `describeParentCallSite@describeParentCallSite.js:1:11
|
||||
parent@parent.js:2:22
|
||||
caller@parent-caller.js:3:33
|
||||
code@other.js:3:33`;
|
||||
}
|
||||
}
|
||||
|
||||
class FirefoxError {
|
||||
name = 'Error';
|
||||
message = 'eHgtF5hmbrXyiEvo';
|
||||
get stack() {
|
||||
return `describeParentCallSite@describeParentCallSite.js:1:11
|
||||
parent@parent.js:2:22
|
||||
caller@parent-caller.js:3:33
|
||||
@other.js:3:33
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
describe('describeParentCallSite', () => {
|
||||
it('should work in Chrome', () => {
|
||||
function myFactory() {
|
||||
return describeParentCallSite(ChromeError);
|
||||
}
|
||||
function myCaller() {
|
||||
return myFactory();
|
||||
}
|
||||
expect(myCaller()).toBe('parent-caller.js:3:33');
|
||||
});
|
||||
|
||||
it('should work in Safari', () => {
|
||||
function myFactory() {
|
||||
return describeParentCallSite(SafariError);
|
||||
}
|
||||
function myCaller() {
|
||||
return myFactory();
|
||||
}
|
||||
expect(myCaller()).toBe('parent-caller.js:3:33');
|
||||
});
|
||||
|
||||
it('should work in Firefox', () => {
|
||||
function myFactory() {
|
||||
return describeParentCallSite(FirefoxError);
|
||||
}
|
||||
function myCaller() {
|
||||
return myFactory();
|
||||
}
|
||||
expect(myCaller()).toBe('parent-caller.js:3:33');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const MESSAGE_MARKER = 'eHgtF5hmbrXyiEvo';
|
||||
|
||||
/**
|
||||
* Internal helper that describes the location of the parent caller.
|
||||
* @internal
|
||||
*/
|
||||
export function describeParentCallSite(
|
||||
ErrorConstructor: { new (message: string): Error } = Error,
|
||||
): string {
|
||||
const { stack } = new ErrorConstructor(MESSAGE_MARKER);
|
||||
if (!stack) {
|
||||
return '<unknown>';
|
||||
}
|
||||
|
||||
// Safari and Firefox don't include the error itself in the stack
|
||||
const startIndex = stack.includes(MESSAGE_MARKER)
|
||||
? stack.indexOf('\n') + 1
|
||||
: 0;
|
||||
const secondEntryStart =
|
||||
stack.indexOf('\n', stack.indexOf('\n', startIndex) + 1) + 1;
|
||||
const secondEntryEnd = stack.indexOf('\n', secondEntryStart);
|
||||
|
||||
const line = stack.substring(secondEntryStart, secondEntryEnd).trim();
|
||||
if (!line) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// Below we try to extract the location for different browsers.
|
||||
// Since RouteRefs are declared at the top-level of modules the caller name isn't interesting.
|
||||
|
||||
// Chrome
|
||||
if (line.includes('(')) {
|
||||
return line.substring(line.indexOf('(') + 1, line.indexOf(')'));
|
||||
}
|
||||
|
||||
// Safari & Firefox
|
||||
if (line.includes('@')) {
|
||||
return line.substring(line.indexOf('@') + 1);
|
||||
}
|
||||
|
||||
// Give up
|
||||
return line;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2020 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 type { AnyRouteRefParams } from './types';
|
||||
export { createRouteRef, type RouteRef } from './RouteRef';
|
||||
export { createSubRouteRef, type SubRouteRef } from './SubRouteRef';
|
||||
export {
|
||||
createExternalRouteRef,
|
||||
type ExternalRouteRef,
|
||||
} from './ExternalRouteRef';
|
||||
export { useRouteRef, type RouteFunc } from './useRouteRef';
|
||||
export { useRouteRefParams } from './useRouteRefParams';
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2020 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Catch-all type for route params.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AnyRouteRefParams = { [param in string]: string } | undefined;
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2020 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
import { MemoryRouter, Router } from 'react-router-dom';
|
||||
import { createVersionedContextForTesting } from '@backstage/version-bridge';
|
||||
import { useRouteRef } from './useRouteRef';
|
||||
import { createRouteRef } from './RouteRef';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
describe('v1 consumer', () => {
|
||||
const context = createVersionedContextForTesting('routing-context');
|
||||
|
||||
afterEach(() => {
|
||||
context.reset();
|
||||
});
|
||||
|
||||
it('should resolve routes', () => {
|
||||
const resolve = jest.fn(() => () => '/hello');
|
||||
context.set({ 1: { resolve } });
|
||||
|
||||
const routeRef = createRouteRef();
|
||||
|
||||
const renderedHook = renderHook(() => useRouteRef(routeRef), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<MemoryRouter initialEntries={['/my-page']} children={children} />
|
||||
),
|
||||
});
|
||||
|
||||
const routeFunc = renderedHook.result.current;
|
||||
expect(routeFunc()).toBe('/hello');
|
||||
expect(resolve).toHaveBeenCalledWith(
|
||||
routeRef,
|
||||
expect.objectContaining({
|
||||
pathname: '/my-page',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('re-resolves the routeFunc when the search parameters change', () => {
|
||||
const resolve = jest.fn(() => () => '/hello');
|
||||
context.set({ 1: { resolve } });
|
||||
|
||||
const routeRef = createRouteRef();
|
||||
const history = createBrowserHistory();
|
||||
history.push('/my-page');
|
||||
|
||||
const { rerender } = renderHook(() => useRouteRef(routeRef), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(1);
|
||||
|
||||
history.push('/my-new-page');
|
||||
rerender();
|
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not re-resolve the routeFunc the location pathname does not change', () => {
|
||||
const resolve = jest.fn(() => () => '/hello');
|
||||
context.set({ 1: { resolve } });
|
||||
|
||||
const routeRef = createRouteRef();
|
||||
const history = createBrowserHistory();
|
||||
history.push('/my-page');
|
||||
|
||||
const { rerender } = renderHook(() => useRouteRef(routeRef), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(1);
|
||||
|
||||
history.push('/my-page');
|
||||
rerender();
|
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not re-resolve the routeFunc when the search parameter changes', () => {
|
||||
const resolve = jest.fn(() => () => '/hello');
|
||||
context.set({ 1: { resolve } });
|
||||
|
||||
const routeRef = createRouteRef();
|
||||
const history = createBrowserHistory();
|
||||
history.push('/my-page');
|
||||
|
||||
const { rerender } = renderHook(() => useRouteRef(routeRef), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(1);
|
||||
|
||||
history.push('/my-page?foo=bar');
|
||||
rerender();
|
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not re-resolve the routeFunc when the hash parameter changes', () => {
|
||||
const resolve = jest.fn(() => () => '/hello');
|
||||
context.set({ 1: { resolve } });
|
||||
|
||||
const routeRef = createRouteRef();
|
||||
const history = createBrowserHistory();
|
||||
history.push('/my-page');
|
||||
|
||||
const { rerender } = renderHook(() => useRouteRef(routeRef), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(1);
|
||||
|
||||
history.push('/my-page#foo');
|
||||
rerender();
|
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2020 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 { useMemo } from 'react';
|
||||
import { matchRoutes, useLocation } from 'react-router-dom';
|
||||
import { useVersionedContext } from '@backstage/version-bridge';
|
||||
import { AnyRouteRefParams } from './types';
|
||||
import { RouteRef } from './RouteRef';
|
||||
import { SubRouteRef } from './SubRouteRef';
|
||||
import { ExternalRouteRef } from './ExternalRouteRef';
|
||||
|
||||
/**
|
||||
* TS magic for handling route parameters.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* The extra TS magic here is to require a single params argument if the RouteRef
|
||||
* had at least one param defined, but require 0 arguments if there are no params defined.
|
||||
* Without this we'd have to pass in empty object to all parameter-less RouteRefs
|
||||
* just to make TypeScript happy, or we would have to make the argument optional in
|
||||
* which case you might forget to pass it in when it is actually required.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RouteFunc<TParams extends AnyRouteRefParams> = (
|
||||
...[params]: TParams extends undefined
|
||||
? readonly []
|
||||
: readonly [params: TParams]
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface RouteResolver {
|
||||
resolve<TParams extends AnyRouteRefParams>(
|
||||
anyRouteRef:
|
||||
| RouteRef<TParams>
|
||||
| SubRouteRef<TParams>
|
||||
| ExternalRouteRef<TParams, any>,
|
||||
sourceLocation: Parameters<typeof matchRoutes>[1],
|
||||
): RouteFunc<TParams> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for constructing URLs to routes.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://backstage.io/docs/plugins/composability#routing-system}
|
||||
*
|
||||
* @param routeRef - The ref to route that should be converted to URL.
|
||||
* @returns A function that will in turn return the concrete URL of the `routeRef`.
|
||||
* @public
|
||||
*/
|
||||
export function useRouteRef<
|
||||
TOptional extends boolean,
|
||||
TParams extends AnyRouteRefParams,
|
||||
>(
|
||||
routeRef: ExternalRouteRef<TParams, TOptional>,
|
||||
): TParams extends true ? RouteFunc<TParams> | undefined : RouteFunc<TParams>;
|
||||
|
||||
/**
|
||||
* React hook for constructing URLs to routes.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://backstage.io/docs/plugins/composability#routing-system}
|
||||
*
|
||||
* @param routeRef - The ref to route that should be converted to URL.
|
||||
* @returns A function that will in turn return the concrete URL of the `routeRef`.
|
||||
* @public
|
||||
*/
|
||||
export function useRouteRef<TParams extends AnyRouteRefParams>(
|
||||
routeRef: RouteRef<TParams> | SubRouteRef<TParams>,
|
||||
): RouteFunc<TParams>;
|
||||
|
||||
/**
|
||||
* React hook for constructing URLs to routes.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://backstage.io/docs/plugins/composability#routing-system}
|
||||
*
|
||||
* @param routeRef - The ref to route that should be converted to URL.
|
||||
* @returns A function that will in turn return the concrete URL of the `routeRef`.
|
||||
* @public
|
||||
*/
|
||||
export function useRouteRef<TParams extends AnyRouteRefParams>(
|
||||
routeRef:
|
||||
| RouteRef<TParams>
|
||||
| SubRouteRef<TParams>
|
||||
| ExternalRouteRef<TParams, any>,
|
||||
): RouteFunc<TParams> | undefined {
|
||||
const { pathname } = useLocation();
|
||||
const versionedContext = useVersionedContext<{ 1: RouteResolver }>(
|
||||
'routing-context',
|
||||
);
|
||||
if (!versionedContext) {
|
||||
throw new Error('Routing context is not available');
|
||||
}
|
||||
|
||||
const resolver = versionedContext.atVersion(1);
|
||||
const routeFunc = useMemo(
|
||||
() => resolver && resolver.resolve(routeRef, { pathname }),
|
||||
[resolver, routeRef, pathname],
|
||||
);
|
||||
|
||||
if (!versionedContext) {
|
||||
throw new Error('useRouteRef used outside of routing context');
|
||||
}
|
||||
if (!resolver) {
|
||||
throw new Error('RoutingContext v1 not available');
|
||||
}
|
||||
|
||||
const isOptional = 'optional' in routeRef && routeRef.optional;
|
||||
if (!routeFunc && !isOptional) {
|
||||
throw new Error(`No path for ${routeRef}`);
|
||||
}
|
||||
|
||||
return routeFunc;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2021 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { useRouteRefParams } from './useRouteRefParams';
|
||||
import { createRouteRef } from './RouteRef';
|
||||
|
||||
describe('useRouteRefParams', () => {
|
||||
it('should provide types params', () => {
|
||||
const routeRef = createRouteRef({
|
||||
params: ['a', 'b'],
|
||||
});
|
||||
|
||||
const Page = () => {
|
||||
const params: { a: string; b: string } = useRouteRefParams(routeRef);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>{params.a}</span>
|
||||
<span>{params.b}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { getByText } = render(
|
||||
<MemoryRouter initialEntries={['/foo/bar']}>
|
||||
<Routes>
|
||||
<Route path="/:a/:b" element={<Page />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(getByText('foo')).toBeInTheDocument();
|
||||
expect(getByText('bar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2021 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 { useParams } from 'react-router-dom';
|
||||
import { AnyRouteRefParams } from './types';
|
||||
import { RouteRef } from './RouteRef';
|
||||
import { SubRouteRef } from './SubRouteRef';
|
||||
|
||||
/**
|
||||
* React hook for retrieving dynamic params from the current URL.
|
||||
* @param _routeRef - Ref of the current route.
|
||||
* @public
|
||||
*/
|
||||
export function useRouteRefParams<Params extends AnyRouteRefParams>(
|
||||
_routeRef: RouteRef<Params> | SubRouteRef<Params>,
|
||||
): Params {
|
||||
return useParams() as Params;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TODO(Rugvip): This might be a quite useful utility type, maybe add to @backstage/types?
|
||||
/**
|
||||
* Utility type to expand type aliases into their equivalent type.
|
||||
* @ignore
|
||||
*/
|
||||
export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
||||
@@ -19,15 +19,15 @@ import {
|
||||
AnyApiFactory,
|
||||
AppTheme,
|
||||
IconComponent,
|
||||
RouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { createExtensionDataRef } from './createExtensionDataRef';
|
||||
import { RouteRef } from '../routing';
|
||||
|
||||
/** @public */
|
||||
export type NavTarget = {
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
routeRef: RouteRef<{}>;
|
||||
routeRef: RouteRef<undefined>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { PortableSchema } from '../schema';
|
||||
import { Expand } from '../types';
|
||||
import { ExtensionDataRef } from './createExtensionDataRef';
|
||||
import { ExtensionInput } from './createExtensionInput';
|
||||
import { BackstagePlugin } from './createPlugin';
|
||||
@@ -32,13 +33,6 @@ export type AnyExtensionInputMap = {
|
||||
>;
|
||||
};
|
||||
|
||||
// TODO(Rugvip): This might be a quite useful utility type, maybe add to @backstage/types?
|
||||
/**
|
||||
* Utility type to expand type aliases into their equivalent type.
|
||||
* @ignore
|
||||
*/
|
||||
export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
||||
|
||||
/**
|
||||
* Converts an extension data map into the matching concrete data values type.
|
||||
* @public
|
||||
|
||||
@@ -14,8 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AnyExternalRoutes, AnyRoutes } from '@backstage/core-plugin-api';
|
||||
import { Extension } from './createExtension';
|
||||
import { ExternalRouteRef, RouteRef } from '../routing';
|
||||
|
||||
/** @public */
|
||||
export type AnyRoutes = { [name in string]: RouteRef };
|
||||
|
||||
/** @public */
|
||||
export type AnyExternalRoutes = { [name in string]: ExternalRouteRef };
|
||||
|
||||
/** @public */
|
||||
export interface PluginOptions<
|
||||
@@ -42,8 +48,8 @@ export interface BackstagePlugin<
|
||||
|
||||
/** @public */
|
||||
export function createPlugin<
|
||||
Routes extends AnyRoutes = AnyRoutes,
|
||||
ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes,
|
||||
Routes extends AnyRoutes = {},
|
||||
ExternalRoutes extends AnyExternalRoutes = {},
|
||||
>(
|
||||
options: PluginOptions<Routes, ExternalRoutes>,
|
||||
): BackstagePlugin<Routes, ExternalRoutes> {
|
||||
|
||||
@@ -37,6 +37,8 @@ export {
|
||||
createPlugin,
|
||||
type BackstagePlugin,
|
||||
type PluginOptions,
|
||||
type AnyRoutes,
|
||||
type AnyExternalRoutes,
|
||||
} from './createPlugin';
|
||||
export {
|
||||
createExtensionOverrides,
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnyExternalRoutes } from '@backstage/core-plugin-api';
|
||||
import { AnyRoutes } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
@@ -26,7 +24,7 @@ export const adrTranslationRef: TranslationRef<
|
||||
>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<AnyRoutes, AnyExternalRoutes>;
|
||||
const _default: BackstagePlugin<{}, {}>;
|
||||
export default _default;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnyExternalRoutes } from '@backstage/core-plugin-api';
|
||||
import { AnyRoutes } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
|
||||
@@ -17,7 +15,7 @@ export const CatalogSearchResultListItemExtension: Extension<{
|
||||
}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<AnyRoutes, AnyExternalRoutes>;
|
||||
const _default: BackstagePlugin<{}, {}>;
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnyExternalRoutes } from '@backstage/core-plugin-api';
|
||||
import { AnyRoutes } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<AnyRoutes, AnyExternalRoutes>;
|
||||
const _default: BackstagePlugin<{}, {}>;
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnyExternalRoutes } from '@backstage/core-plugin-api';
|
||||
import { AnyRoutes } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { GraphQLEndpoint } from '@backstage/plugin-graphiql';
|
||||
import { PortableSchema } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export function createEndpointExtension<TConfig extends {}>(options: {
|
||||
@@ -21,7 +20,12 @@ export function createEndpointExtension<TConfig extends {}>(options: {
|
||||
}): Extension<TConfig>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<AnyRoutes, AnyExternalRoutes>;
|
||||
const _default: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
|
||||
@@ -32,19 +32,15 @@ import {
|
||||
GraphQLEndpoint,
|
||||
GraphiQLIcon,
|
||||
} from '@backstage/plugin-graphiql';
|
||||
import {
|
||||
createApiFactory,
|
||||
createRouteRef,
|
||||
IconComponent,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
const graphiqlRouteRef = createRouteRef({ id: 'plugin.graphiql.page' });
|
||||
import { createApiFactory, IconComponent } from '@backstage/core-plugin-api';
|
||||
import { graphiQLRouteRef } from './route-refs';
|
||||
import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export const GraphiqlPage = createPageExtension({
|
||||
id: 'plugin.graphiql.page',
|
||||
defaultPath: '/graphiql',
|
||||
routeRef: graphiqlRouteRef,
|
||||
routeRef: convertLegacyRouteRef(graphiQLRouteRef),
|
||||
loader: () => import('./components').then(m => <m.GraphiQLPage />),
|
||||
});
|
||||
|
||||
@@ -53,7 +49,7 @@ export const graphiqlPageSidebarItem = createNavItemExtension({
|
||||
id: 'plugin.graphiql.nav.index',
|
||||
title: 'GraphiQL',
|
||||
icon: GraphiQLIcon as IconComponent,
|
||||
routeRef: graphiqlRouteRef,
|
||||
routeRef: convertLegacyRouteRef(graphiQLRouteRef),
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
@@ -125,4 +121,7 @@ export default createPlugin({
|
||||
gitlabGraphiQLBrowseExtension,
|
||||
graphiqlPageSidebarItem,
|
||||
],
|
||||
routes: {
|
||||
root: convertLegacyRouteRef(graphiQLRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnyExternalRoutes } from '@backstage/core-plugin-api';
|
||||
import { AnyRoutes } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<AnyRoutes, AnyExternalRoutes>;
|
||||
const _default: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
useSidebarPinState,
|
||||
} from '@backstage/core-components';
|
||||
import {
|
||||
createRouteRef,
|
||||
useApi,
|
||||
DiscoveryApi,
|
||||
IdentityApi,
|
||||
@@ -64,9 +63,11 @@ import { SearchResult } from '@backstage/plugin-search-common';
|
||||
import { searchApiRef } from '@backstage/plugin-search-react';
|
||||
import { searchResultItemExtensionData } from '@backstage/plugin-search-react/alpha';
|
||||
|
||||
import { rootRouteRef } from './plugin';
|
||||
import { SearchClient } from './apis';
|
||||
import { SearchType } from './components/SearchType';
|
||||
import { UrlUpdater } from './components/SearchPage/SearchPage';
|
||||
import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export const SearchApi = createApiExtension({
|
||||
@@ -95,12 +96,10 @@ const useSearchPageStyles = makeStyles((theme: Theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const searchRouteRef = createRouteRef({ id: 'plugin.search.page' });
|
||||
|
||||
/** @alpha */
|
||||
export const SearchPage = createPageExtension({
|
||||
id: 'plugin.search.page',
|
||||
routeRef: searchRouteRef,
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
path: z.string().default('/search'),
|
||||
@@ -237,7 +236,7 @@ export const SearchPage = createPageExtension({
|
||||
/** @alpha */
|
||||
export const SearchNavItem = createNavItemExtension({
|
||||
id: 'plugin.search.nav.index',
|
||||
routeRef: searchRouteRef,
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
title: 'Search',
|
||||
icon: SearchIcon,
|
||||
});
|
||||
@@ -246,4 +245,7 @@ export const SearchNavItem = createNavItemExtension({
|
||||
export default createPlugin({
|
||||
id: 'plugin.search',
|
||||
extensions: [SearchApi, SearchPage, SearchNavItem],
|
||||
routes: {
|
||||
root: convertLegacyRouteRef(rootRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnyExternalRoutes } from '@backstage/core-plugin-api';
|
||||
import { AnyRoutes } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<AnyRoutes, AnyExternalRoutes>;
|
||||
const _default: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createApiFactory, createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { createApiFactory } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
createApiExtension,
|
||||
createPageExtension,
|
||||
@@ -24,14 +24,14 @@ import {
|
||||
import React from 'react';
|
||||
import { techRadarApiRef } from './api';
|
||||
import { SampleTechRadarApi } from './sample';
|
||||
|
||||
const techRadarRouteRef = createRouteRef({ id: 'plugin.techradar.page' });
|
||||
import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { rootRouteRef } from './plugin';
|
||||
|
||||
/** @alpha */
|
||||
export const TechRadarPage = createPageExtension({
|
||||
id: 'plugin.techradar.page',
|
||||
defaultPath: '/tech-radar',
|
||||
routeRef: techRadarRouteRef,
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
title: z.string().default('Tech Radar'),
|
||||
@@ -59,4 +59,7 @@ const sampleTechRadarApi = createApiExtension({
|
||||
export default createPlugin({
|
||||
id: 'tech-radar',
|
||||
extensions: [TechRadarPage, sampleTechRadarApi],
|
||||
routes: {
|
||||
root: convertLegacyRouteRef(rootRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
createApiFactory,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
const rootRouteRef = createRouteRef({
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'tech-radar',
|
||||
});
|
||||
|
||||
|
||||
@@ -3,13 +3,22 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnyExternalRoutes } from '@backstage/core-plugin-api';
|
||||
import { AnyRoutes } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<AnyRoutes, AnyExternalRoutes>;
|
||||
const _default: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
docRoot: RouteRef<{
|
||||
name: string;
|
||||
kind: string;
|
||||
namespace: string;
|
||||
}>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
techdocsStorageApiRef,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { TechDocsClient, TechDocsStorageClient } from './client';
|
||||
import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
const rootRouteRef = createRouteRef({
|
||||
id: 'plugin.techdocs.indexPage',
|
||||
@@ -76,7 +77,7 @@ export const TechDocsSearchResultListItemExtension =
|
||||
const TechDocsIndexPage = createPageExtension({
|
||||
id: 'plugin.techdocs.indexPage',
|
||||
defaultPath: '/docs',
|
||||
routeRef: rootRouteRef,
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
loader: () =>
|
||||
import('./home/components/TechDocsIndexPage').then(m => (
|
||||
<m.TechDocsIndexPage />
|
||||
@@ -94,7 +95,7 @@ const TechDocsReaderPage = createPageExtension({
|
||||
import('./reader/components/TechDocsReaderPage').then(m => (
|
||||
<m.TechDocsReaderPage />
|
||||
)),
|
||||
routeRef: rootDocsRouteRef,
|
||||
routeRef: convertLegacyRouteRef(rootDocsRouteRef),
|
||||
defaultPath: '/docs/:namespace/:kind/:name/*',
|
||||
});
|
||||
|
||||
@@ -153,4 +154,8 @@ export default createPlugin({
|
||||
techDocsStorage,
|
||||
TechDocsSearchResultListItemExtension,
|
||||
],
|
||||
routes: {
|
||||
root: convertLegacyRouteRef(rootRouteRef),
|
||||
docRoot: convertLegacyRouteRef(rootDocsRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnyExternalRoutes } from '@backstage/core-plugin-api';
|
||||
import { AnyRoutes } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<AnyRoutes, AnyExternalRoutes>;
|
||||
const _default: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const userSettingsRouteRef: RouteRef<undefined>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const userSettingsTranslationRef: TranslationRef<
|
||||
'user-settings',
|
||||
|
||||
@@ -13,29 +13,23 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
createPageExtension,
|
||||
createPlugin,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { settingsRouteRef } from './plugin';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export * from './translation';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const userSettingsRouteRef = createRouteRef({
|
||||
id: 'plugin.user-settings.page',
|
||||
});
|
||||
|
||||
const UserSettingsPage = createPageExtension({
|
||||
id: 'plugin.user-settings.page',
|
||||
defaultPath: '/settings',
|
||||
routeRef: userSettingsRouteRef,
|
||||
routeRef: convertLegacyRouteRef(settingsRouteRef),
|
||||
inputs: {
|
||||
providerSettings: createExtensionInput(
|
||||
{
|
||||
@@ -56,4 +50,7 @@ const UserSettingsPage = createPageExtension({
|
||||
export default createPlugin({
|
||||
id: 'user-settings',
|
||||
extensions: [UserSettingsPage],
|
||||
routes: {
|
||||
root: convertLegacyRouteRef(settingsRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4234,9 +4234,12 @@ __metadata:
|
||||
"@backstage/frontend-app-api": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@backstage/version-bridge": "workspace:^"
|
||||
"@testing-library/jest-dom": ^6.0.0
|
||||
"@testing-library/react": ^12.1.3
|
||||
"@testing-library/react-hooks": ^8.0.1
|
||||
"@types/react": ^16.13.1 || ^17.0.0
|
||||
history: ^5.3.0
|
||||
lodash: ^4.17.21
|
||||
zod: ^3.21.4
|
||||
zod-to-json-schema: ^3.21.4
|
||||
@@ -27641,7 +27644,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"history@npm:^5.0.0":
|
||||
"history@npm:^5.0.0, history@npm:^5.3.0":
|
||||
version: 5.3.0
|
||||
resolution: "history@npm:5.3.0"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user