app-api: refactor to depend on plugin-api and prune duplicates
Co-authored-by: Juan Lulkin <jmaiz@spotify.com> Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createApiRef } from '@backstage/plugin-api';
|
||||
import { ApiAggregator } from './ApiAggregator';
|
||||
import { createApiRef } from './ApiRef';
|
||||
import { ApiRegistry } from './ApiRegistry';
|
||||
|
||||
describe('ApiAggregator', () => {
|
||||
const apiARef = createApiRef<number>({ id: 'a', description: '' });
|
||||
const apiBRef = createApiRef<number>({ id: 'b', description: '' });
|
||||
const apiARef = createApiRef<number>({ id: 'a' });
|
||||
const apiBRef = createApiRef<number>({ id: 'b' });
|
||||
|
||||
it('should forward implementations', () => {
|
||||
const agg = new ApiAggregator(
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiRef, ApiHolder } from './types';
|
||||
import { ApiRef, ApiHolder } from '@backstage/plugin-api';
|
||||
|
||||
/**
|
||||
* An ApiHolder that queries multiple other holders from for
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createApiRef } from '@backstage/plugin-api';
|
||||
import { ApiFactoryRegistry } from './ApiFactoryRegistry';
|
||||
import { createApiRef } from './ApiRef';
|
||||
|
||||
const aRef = createApiRef<number>({ id: 'a', description: '' });
|
||||
const aRef = createApiRef<number>({ id: 'a' });
|
||||
const aFactory1 = { api: aRef, deps: {}, factory: () => 1 };
|
||||
const aFactory2 = { api: aRef, deps: {}, factory: () => 2 };
|
||||
const bRef = createApiRef<string>({ id: 'b', description: '' });
|
||||
const bRef = createApiRef<string>({ id: 'b' });
|
||||
const bFactory = { api: bRef, deps: {}, factory: () => 'x' };
|
||||
const cRef = createApiRef<string>({ id: 'c', description: '' });
|
||||
const cRef = createApiRef<string>({ id: 'c' });
|
||||
const cFactory = { api: cRef, deps: {}, factory: () => 'y' };
|
||||
|
||||
describe('ApiFactoryRegistry', () => {
|
||||
@@ -69,8 +69,8 @@ describe('ApiFactoryRegistry', () => {
|
||||
});
|
||||
|
||||
it('should identify ApiRefs by id but still return the correct factory ref when listing all apis', () => {
|
||||
const ref1 = createApiRef<number>({ id: 'a', description: 'ref1' });
|
||||
const ref2 = createApiRef<number>({ id: 'a', description: 'ref2' });
|
||||
const ref1 = createApiRef<number>({ id: 'a' });
|
||||
const ref2 = createApiRef<number>({ id: 'a' });
|
||||
|
||||
const factory1 = { api: ref1, deps: {}, factory: () => 3 };
|
||||
const factory2 = { api: ref2, deps: {}, factory: () => 3 };
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiFactoryHolder } from './types';
|
||||
import {
|
||||
ApiRef,
|
||||
ApiFactoryHolder,
|
||||
ApiFactory,
|
||||
AnyApiRef,
|
||||
AnyApiFactory,
|
||||
} from './types';
|
||||
} from '@backstage/plugin-api';
|
||||
|
||||
type ApiFactoryScope =
|
||||
| 'default' // Default factories registered by core and plugins
|
||||
|
||||
@@ -15,18 +15,23 @@
|
||||
*/
|
||||
|
||||
import React, { Context, useContext } from 'react';
|
||||
import { ApiProvider, useApi, withApis } from './ApiProvider';
|
||||
import { createApiRef } from './ApiRef';
|
||||
import {
|
||||
useApi,
|
||||
createApiRef,
|
||||
withApis,
|
||||
ApiHolder,
|
||||
ApiRef,
|
||||
} from '@backstage/plugin-api';
|
||||
import { ApiProvider } from './ApiProvider';
|
||||
import { ApiRegistry } from './ApiRegistry';
|
||||
import { render } from '@testing-library/react';
|
||||
import { withLogCollector } from '@backstage/test-utils-core';
|
||||
import { getGlobalSingleton } from '../../lib/globalObject';
|
||||
import { ApiHolder, ApiRef } from './types';
|
||||
import { VersionedValue } from '../../lib/versionedValues';
|
||||
|
||||
describe('ApiProvider', () => {
|
||||
type Api = () => string;
|
||||
const apiRef = createApiRef<Api>({ id: 'x', description: '' });
|
||||
const apiRef = createApiRef<Api>({ id: 'x' });
|
||||
const registry = ApiRegistry.from([[apiRef, () => 'hello']]);
|
||||
|
||||
const MyHookConsumer = () => {
|
||||
@@ -55,8 +60,8 @@ describe('ApiProvider', () => {
|
||||
});
|
||||
|
||||
it('should provide nested access to apis', () => {
|
||||
const aRef = createApiRef<string>({ id: 'a', description: '' });
|
||||
const bRef = createApiRef<string>({ id: 'b', description: '' });
|
||||
const aRef = createApiRef<string>({ id: 'a' });
|
||||
const bRef = createApiRef<string>({ id: 'b' });
|
||||
|
||||
const MyComponent = () => {
|
||||
const a = useApi(aRef);
|
||||
@@ -85,7 +90,7 @@ describe('ApiProvider', () => {
|
||||
|
||||
it('should ignore deps in prototype', () => {
|
||||
// 100% coverage + happy typescript = hasOwnProperty + this atrocity
|
||||
const xRef = createApiRef<number>({ id: 'x', description: '' });
|
||||
const xRef = createApiRef<number>({ id: 'x' });
|
||||
|
||||
const proto = { x: xRef };
|
||||
const props = { getMessage: { enumerable: true, value: apiRef } };
|
||||
@@ -193,7 +198,7 @@ describe('v1 consumer', () => {
|
||||
}
|
||||
|
||||
type Api = () => string;
|
||||
const apiRef = createApiRef<Api>({ id: 'x', description: '' });
|
||||
const apiRef = createApiRef<Api>({ id: 'x' });
|
||||
const registry = ApiRegistry.from([[apiRef, () => 'hello']]);
|
||||
|
||||
const MyHookConsumerV1 = () => {
|
||||
|
||||
@@ -19,26 +19,16 @@ import React, {
|
||||
useContext,
|
||||
ReactNode,
|
||||
PropsWithChildren,
|
||||
Context,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ApiRef, ApiHolder, TypesToApiRefs } from './types';
|
||||
import { ApiHolder } from '@backstage/plugin-api';
|
||||
import { ApiAggregator } from './ApiAggregator';
|
||||
import {
|
||||
getGlobalSingleton,
|
||||
getOrCreateGlobalSingleton,
|
||||
} from '../../lib/globalObject';
|
||||
import { getOrCreateGlobalSingleton } from '../../lib/globalObject';
|
||||
import {
|
||||
VersionedValue,
|
||||
createVersionedValueMap,
|
||||
} from '../../lib/versionedValues';
|
||||
|
||||
const missingHolderMessage =
|
||||
'No ApiProvider available in react context. ' +
|
||||
'A common cause of this error is that multiple versions of @backstage/core-api are installed. ' +
|
||||
`You can check if that is the case using 'yarn backstage-cli versions:check', and can in many cases ` +
|
||||
`fix the issue either with the --fix flag or using 'yarn backstage-cli versions:bump'`;
|
||||
|
||||
type ApiProviderProps = {
|
||||
apis: ApiHolder;
|
||||
children: ReactNode;
|
||||
@@ -68,62 +58,3 @@ ApiProvider.propTypes = {
|
||||
apis: PropTypes.shape({ get: PropTypes.func.isRequired }).isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export function useApiHolder(): ApiHolder {
|
||||
const versionedHolder = useContext(
|
||||
getGlobalSingleton<Context<ApiContextType>>('api-context'),
|
||||
);
|
||||
|
||||
if (!versionedHolder) {
|
||||
throw new Error(missingHolderMessage);
|
||||
}
|
||||
|
||||
const apiHolder = versionedHolder.atVersion(1);
|
||||
if (!apiHolder) {
|
||||
throw new Error('ApiContext v1 not available');
|
||||
}
|
||||
|
||||
return apiHolder;
|
||||
}
|
||||
|
||||
export function useApi<T>(apiRef: ApiRef<T>): T {
|
||||
const apiHolder = useApiHolder();
|
||||
|
||||
const api = apiHolder.get(apiRef);
|
||||
if (!api) {
|
||||
throw new Error(`No implementation available for ${apiRef}`);
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
export function withApis<T>(apis: TypesToApiRefs<T>) {
|
||||
return function withApisWrapper<P extends T>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
) {
|
||||
const Hoc = (props: PropsWithChildren<Omit<P, keyof T>>) => {
|
||||
const apiHolder = useApiHolder();
|
||||
|
||||
const impls = {} as T;
|
||||
|
||||
for (const key in apis) {
|
||||
if (apis.hasOwnProperty(key)) {
|
||||
const ref = apis[key];
|
||||
|
||||
const api = apiHolder.get(ref);
|
||||
if (!api) {
|
||||
throw new Error(`No implementation available for ${ref}`);
|
||||
}
|
||||
impls[key] = api;
|
||||
}
|
||||
}
|
||||
|
||||
return <WrappedComponent {...(props as P)} {...impls} />;
|
||||
};
|
||||
const displayName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
||||
|
||||
Hoc.displayName = `withApis(${displayName})`;
|
||||
|
||||
return Hoc;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { createApiRef } from './ApiRef';
|
||||
|
||||
describe('ApiRef', () => {
|
||||
it('should be created', () => {
|
||||
const ref = createApiRef({ id: 'abc', description: '123' });
|
||||
expect(ref.id).toBe('abc');
|
||||
expect(ref.description).toBe('123');
|
||||
expect(String(ref)).toBe('apiRef{abc}');
|
||||
expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}');
|
||||
});
|
||||
|
||||
it('should reject invalid ids', () => {
|
||||
for (const id of ['a', 'abc', 'ab-c', 'a.b.c', 'a-b.c', 'abc.a-b-c.abc3']) {
|
||||
expect(createApiRef({ id, description: '123' }).id).toBe(id);
|
||||
}
|
||||
|
||||
for (const id of [
|
||||
'123',
|
||||
'ab-3',
|
||||
'ab_c',
|
||||
'.',
|
||||
'2ac',
|
||||
'ab.3a',
|
||||
'.abc',
|
||||
'abc.',
|
||||
'ab..s',
|
||||
'',
|
||||
'_',
|
||||
]) {
|
||||
expect(() => createApiRef({ id, description: '123' }).id).toThrow(
|
||||
`API id must only contain period separated lowercase alphanum tokens with dashes, got '${id}'`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 type { ApiRef } from './types';
|
||||
|
||||
export type ApiRefConfig = {
|
||||
id: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
class ApiRefImpl<T> implements ApiRef<T> {
|
||||
constructor(private readonly config: ApiRefConfig) {
|
||||
const valid = config.id
|
||||
.split('.')
|
||||
.flatMap(part => part.split('-'))
|
||||
.every(part => part.match(/^[a-z][a-z0-9]*$/));
|
||||
if (!valid) {
|
||||
throw new Error(
|
||||
`API id must only contain period separated lowercase alphanum tokens with dashes, got '${config.id}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.config.id;
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.config.description;
|
||||
}
|
||||
|
||||
// Utility for getting type of an api, using `typeof apiRef.T`
|
||||
get T(): T {
|
||||
throw new Error(`tried to read ApiRef.T of ${this}`);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `apiRef{${this.config.id}}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createApiRef<T>(config: ApiRefConfig): ApiRef<T> {
|
||||
return new ApiRefImpl<T>(config);
|
||||
}
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createApiRef } from '@backstage/plugin-api';
|
||||
import { ApiRegistry } from './ApiRegistry';
|
||||
import { createApiRef } from './ApiRef';
|
||||
|
||||
describe('ApiRegistry', () => {
|
||||
const x1Ref = createApiRef<number>({ id: 'x1', description: '' });
|
||||
const x1DuplicateRef = createApiRef<number>({ id: 'x1', description: '' });
|
||||
const x2Ref = createApiRef<string>({ id: 'x2', description: '' });
|
||||
const x1Ref = createApiRef<number>({ id: 'x1' });
|
||||
const x1DuplicateRef = createApiRef<number>({ id: 'x1' });
|
||||
const x2Ref = createApiRef<string>({ id: 'x2' });
|
||||
|
||||
it('should be created', () => {
|
||||
const registry = ApiRegistry.from([]);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiRef, ApiHolder } from './types';
|
||||
import { ApiRef, ApiHolder } from '@backstage/plugin-api';
|
||||
|
||||
type ApiImpl<T = unknown> = readonly [ApiRef<T>, T];
|
||||
|
||||
|
||||
@@ -14,19 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createApiRef } from '@backstage/plugin-api';
|
||||
import { ApiResolver } from './ApiResolver';
|
||||
import { createApiRef } from './ApiRef';
|
||||
import { ApiFactoryRegistry } from './ApiFactoryRegistry';
|
||||
|
||||
const aRef = createApiRef<number>({ id: 'a', description: '' });
|
||||
const otherARef = createApiRef<number>({ id: 'a', description: 'other' });
|
||||
const bRef = createApiRef<string>({ id: 'b', description: '' });
|
||||
const otherBRef = createApiRef<string>({ id: 'b', description: 'other' });
|
||||
const cRef = createApiRef<{ x: string }>({ id: 'c', description: '' });
|
||||
const otherCRef = createApiRef<{ x: string }>({
|
||||
id: 'c',
|
||||
description: 'other',
|
||||
});
|
||||
const aRef = createApiRef<number>({ id: 'a' });
|
||||
const otherARef = createApiRef<number>({ id: 'a' });
|
||||
const bRef = createApiRef<string>({ id: 'b' });
|
||||
const otherBRef = createApiRef<string>({ id: 'b' });
|
||||
const cRef = createApiRef<{ x: string }>({ id: 'c' });
|
||||
const otherCRef = createApiRef<{ x: string }>({ id: 'c' });
|
||||
|
||||
function createRegistry() {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
import {
|
||||
ApiRef,
|
||||
ApiHolder,
|
||||
ApiFactoryHolder,
|
||||
AnyApiRef,
|
||||
TypesToApiRefs,
|
||||
} from './types';
|
||||
} from '@backstage/plugin-api';
|
||||
import { ApiFactoryHolder } from './types';
|
||||
|
||||
export class ApiResolver implements ApiHolder {
|
||||
/**
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { ApiRef, ApiFactory, TypesToApiRefs } from './types';
|
||||
|
||||
/**
|
||||
* Used to infer types for a standalone ApiFactory that isn't immediately passed
|
||||
* to another function.
|
||||
* This function doesn't actually do anything, it's only used to infer types.
|
||||
*/
|
||||
export function createApiFactory<
|
||||
Api,
|
||||
Impl extends Api,
|
||||
Deps extends { [name in string]: unknown }
|
||||
>(factory: ApiFactory<Api, Impl, Deps>): ApiFactory<Api, Impl, Deps>;
|
||||
export function createApiFactory<Api, Impl extends Api>(
|
||||
api: ApiRef<Api>,
|
||||
instance: Impl,
|
||||
): ApiFactory<Api, Impl, {}>;
|
||||
export function createApiFactory<
|
||||
Api,
|
||||
Impl extends Api,
|
||||
Deps extends { [name in string]: unknown }
|
||||
>(
|
||||
factory: ApiFactory<Api, Impl, Deps> | ApiRef<Api>,
|
||||
instance?: Impl,
|
||||
): ApiFactory<Api, Impl, Deps> {
|
||||
if ('id' in factory) {
|
||||
return {
|
||||
api: factory,
|
||||
deps: {} as TypesToApiRefs<Deps>,
|
||||
factory: () => instance!,
|
||||
};
|
||||
}
|
||||
return factory;
|
||||
}
|
||||
@@ -14,10 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ApiProvider, useApi, useApiHolder } from './ApiProvider';
|
||||
export { ApiProvider } from './ApiProvider';
|
||||
export { ApiRegistry } from './ApiRegistry';
|
||||
export { ApiResolver } from './ApiResolver';
|
||||
export { ApiFactoryRegistry } from './ApiFactoryRegistry';
|
||||
export { createApiRef } from './ApiRef';
|
||||
export * from './types';
|
||||
export * from './helpers';
|
||||
|
||||
@@ -14,41 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type ApiRef<T> = {
|
||||
id: string;
|
||||
description?: string;
|
||||
T: T;
|
||||
};
|
||||
|
||||
export type AnyApiRef = ApiRef<unknown>;
|
||||
|
||||
export type ApiRefType<T> = T extends ApiRef<infer U> ? U : never;
|
||||
|
||||
export type TypesToApiRefs<T> = { [key in keyof T]: ApiRef<T[key]> };
|
||||
|
||||
export type ApiRefsToTypes<T extends { [key in string]: ApiRef<unknown> }> = {
|
||||
[key in keyof T]: ApiRefType<T[key]>;
|
||||
};
|
||||
|
||||
export type ApiHolder = {
|
||||
get<T>(api: ApiRef<T>): T | undefined;
|
||||
};
|
||||
|
||||
export type ApiFactory<
|
||||
Api,
|
||||
Impl extends Api,
|
||||
Deps extends { [name in string]: unknown }
|
||||
> = {
|
||||
api: ApiRef<Api>;
|
||||
deps: TypesToApiRefs<Deps>;
|
||||
factory(deps: Deps): Impl;
|
||||
};
|
||||
|
||||
export type AnyApiFactory = ApiFactory<
|
||||
unknown,
|
||||
unknown,
|
||||
{ [key in string]: unknown }
|
||||
>;
|
||||
import { ApiFactory, ApiRef } from '@backstage/plugin-api';
|
||||
|
||||
export type ApiFactoryHolder = {
|
||||
get<T>(
|
||||
|
||||
@@ -21,13 +21,13 @@ import React, { PropsWithChildren } from 'react';
|
||||
import { BrowserRouter, Routes } from 'react-router-dom';
|
||||
import { createRoutableExtension } from '../extensions';
|
||||
import { defaultAppIcons } from './icons';
|
||||
import { createPlugin } from '../plugin';
|
||||
import { useRouteRef } from '../routing/hooks';
|
||||
import {
|
||||
createPlugin,
|
||||
useRouteRef,
|
||||
createExternalRouteRef,
|
||||
createRouteRef,
|
||||
createSubRouteRef,
|
||||
} from '../routing';
|
||||
} from '@backstage/plugin-api';
|
||||
import { generateBoundRoutes, PrivateAppImpl } from './App';
|
||||
|
||||
describe('generateBoundRoutes', () => {
|
||||
|
||||
@@ -21,19 +21,19 @@ import React, {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import {
|
||||
AnyApiFactory,
|
||||
ApiHolder,
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
AppThemeSelector,
|
||||
ConfigReader,
|
||||
LocalStorageFeatureFlags,
|
||||
useApi,
|
||||
} from '../apis';
|
||||
import {
|
||||
useApi,
|
||||
AnyApiFactory,
|
||||
ApiHolder,
|
||||
IconComponent,
|
||||
AppTheme,
|
||||
appThemeApiRef,
|
||||
@@ -42,6 +42,9 @@ import {
|
||||
ConfigApi,
|
||||
featureFlagsApiRef,
|
||||
identityApiRef,
|
||||
BackstagePlugin,
|
||||
RouteRef,
|
||||
ExternalRouteRef,
|
||||
} from '@backstage/plugin-api';
|
||||
import { ApiFactoryRegistry, ApiResolver } from '../apis/system';
|
||||
import {
|
||||
@@ -49,15 +52,12 @@ import {
|
||||
routeElementDiscoverer,
|
||||
traverseElementTree,
|
||||
} from '../extensions/traversal';
|
||||
import { BackstagePlugin } from '../plugin';
|
||||
import { AnyRoutes } from '../plugin/types';
|
||||
import { RouteRef, ExternalRouteRef } from '../routing';
|
||||
import {
|
||||
routeObjectCollector,
|
||||
routeParentCollector,
|
||||
routePathCollector,
|
||||
} from '../routing/collectors';
|
||||
import { RoutingProvider } from '../routing/hooks';
|
||||
import { RoutingProvider } from '../routing/RoutingProvider';
|
||||
import { validateRoutes } from '../routing/validation';
|
||||
import { AppContextProvider } from './AppContext';
|
||||
import { AppIdentity } from './AppIdentity';
|
||||
@@ -79,7 +79,10 @@ export function generateBoundRoutes(
|
||||
const result = new Map<ExternalRouteRef, RouteRef>();
|
||||
|
||||
if (bindRoutes) {
|
||||
const bind: AppRouteBinder = (externalRoutes, targetRoutes: AnyRoutes) => {
|
||||
const bind: AppRouteBinder = (
|
||||
externalRoutes,
|
||||
targetRoutes: { [name: string]: RouteRef<any> },
|
||||
) => {
|
||||
for (const [key, value] of Object.entries(targetRoutes)) {
|
||||
const externalRoute = externalRoutes[key];
|
||||
if (!externalRoute) {
|
||||
@@ -150,12 +153,6 @@ function useConfigLoader(
|
||||
class AppContextImpl implements AppContext {
|
||||
constructor(private readonly app: PrivateAppImpl) {}
|
||||
|
||||
getPlugins(): BackstagePlugin<any, any>[] {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('appContext.getPlugins() is deprecated and will be removed');
|
||||
return this.app.getPlugins();
|
||||
}
|
||||
|
||||
getSystemIcon(key: string): IconComponent | undefined {
|
||||
return this.app.getSystemIcon(key);
|
||||
}
|
||||
@@ -163,24 +160,6 @@ class AppContextImpl implements AppContext {
|
||||
getComponents(): AppComponents {
|
||||
return this.app.getComponents();
|
||||
}
|
||||
|
||||
getProvider(): React.ComponentType<{}> {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('appContext.getProvider() is deprecated and will be removed');
|
||||
return this.app.getProvider();
|
||||
}
|
||||
|
||||
getRouter(): React.ComponentType<{}> {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('appContext.getRouter() is deprecated and will be removed');
|
||||
return this.app.getRouter();
|
||||
}
|
||||
|
||||
getRoutes(): JSX.Element[] {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('appContext.getRoutes() is deprecated and will be removed');
|
||||
return this.app.getRoutes();
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivateAppImpl implements BackstageApp {
|
||||
@@ -221,59 +200,6 @@ export class PrivateAppImpl implements BackstageApp {
|
||||
return this.components;
|
||||
}
|
||||
|
||||
getRoutes(): JSX.Element[] {
|
||||
const routes = new Array<JSX.Element>();
|
||||
|
||||
const { NotFoundErrorPage } = this.components;
|
||||
|
||||
for (const plugin of this.plugins.values()) {
|
||||
for (const output of plugin.output()) {
|
||||
switch (output.type) {
|
||||
case 'legacy-route': {
|
||||
const { path, component: Component } = output;
|
||||
routes.push(
|
||||
<Route key={path} path={path} element={<Component />} />,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'route': {
|
||||
const { target, component: Component } = output;
|
||||
routes.push(
|
||||
<Route
|
||||
key={`${plugin.getId()}-${target.path}`}
|
||||
path={target.path}
|
||||
element={<Component />}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'legacy-redirect-route': {
|
||||
const { path, target } = output;
|
||||
routes.push(<Navigate key={path} to={target} />);
|
||||
break;
|
||||
}
|
||||
case 'redirect-route': {
|
||||
const { from, to } = output;
|
||||
routes.push(<Navigate key={from.path} to={to.path} />);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
routes.push(
|
||||
<Route
|
||||
key="not-found-error-page"
|
||||
path="/*"
|
||||
element={<NotFoundErrorPage />}
|
||||
/>,
|
||||
);
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
getProvider(): ComponentType<{}> {
|
||||
const appContext = new AppContextImpl(this);
|
||||
const apiHolder = this.getApiHolder();
|
||||
|
||||
@@ -38,10 +38,6 @@ describe('v1 consumer', () => {
|
||||
const mockContext: AppContextV1 = {
|
||||
getComponents: jest.fn(),
|
||||
getSystemIcon: jest.fn(),
|
||||
getPlugins: jest.fn(),
|
||||
getProvider: jest.fn(),
|
||||
getRouter: jest.fn(),
|
||||
getRoutes: jest.fn(),
|
||||
};
|
||||
|
||||
const renderedHook = renderHook(() => useMockAppV1(), {
|
||||
@@ -59,21 +55,5 @@ describe('v1 consumer', () => {
|
||||
result.getSystemIcon('icon');
|
||||
expect(mockContext.getSystemIcon).toHaveBeenCalledTimes(1);
|
||||
expect(mockContext.getSystemIcon).toHaveBeenCalledWith('icon');
|
||||
|
||||
expect(mockContext.getPlugins).toHaveBeenCalledTimes(0);
|
||||
result.getPlugins();
|
||||
expect(mockContext.getPlugins).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockContext.getProvider).toHaveBeenCalledTimes(0);
|
||||
result.getProvider();
|
||||
expect(mockContext.getProvider).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockContext.getRouter).toHaveBeenCalledTimes(0);
|
||||
result.getRouter();
|
||||
expect(mockContext.getRouter).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockContext.getRoutes).toHaveBeenCalledTimes(0);
|
||||
result.getRoutes();
|
||||
expect(mockContext.getRoutes).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,20 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
Context,
|
||||
} from 'react';
|
||||
import React, { createContext, PropsWithChildren } from 'react';
|
||||
import {
|
||||
VersionedValue,
|
||||
createVersionedValueMap,
|
||||
} from '../lib/versionedValues';
|
||||
import {
|
||||
getGlobalSingleton,
|
||||
getOrCreateGlobalSingleton,
|
||||
} from '../lib/globalObject';
|
||||
import { getOrCreateGlobalSingleton } from '../lib/globalObject';
|
||||
import { AppContext as AppContextV1 } from './types';
|
||||
|
||||
type AppContextType = VersionedValue<{ 1: AppContextV1 }> | undefined;
|
||||
@@ -47,17 +39,3 @@ export const AppContextProvider = ({
|
||||
|
||||
return <AppContext.Provider value={versionedValue} children={children} />;
|
||||
};
|
||||
|
||||
export const useApp = (): AppContextV1 => {
|
||||
const versionedContext = useContext(
|
||||
getGlobalSingleton<Context<AppContextType>>('app-context'),
|
||||
);
|
||||
if (!versionedContext) {
|
||||
throw new Error('No app context available');
|
||||
}
|
||||
const appContext = versionedContext.atVersion(1);
|
||||
if (!appContext) {
|
||||
throw new Error('AppContext v1 not available');
|
||||
}
|
||||
return appContext;
|
||||
};
|
||||
|
||||
@@ -27,13 +27,13 @@ import {
|
||||
MicrosoftAuth,
|
||||
OAuthRequestManager,
|
||||
WebStorage,
|
||||
createApiFactory,
|
||||
UrlPatternDiscovery,
|
||||
SamlAuth,
|
||||
OneLoginAuth,
|
||||
} from '../apis';
|
||||
|
||||
import {
|
||||
createApiFactory,
|
||||
alertApiRef,
|
||||
errorApiRef,
|
||||
discoveryApiRef,
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { useApp } from './AppContext';
|
||||
export { createApp } from './createApp';
|
||||
export * from './types';
|
||||
|
||||
@@ -15,12 +15,17 @@
|
||||
*/
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import { AnyExternalRoutes, BackstagePlugin } from '../plugin/types';
|
||||
import { ExternalRouteRef, RouteRef } from '../routing';
|
||||
import { AnyApiFactory } from '../apis';
|
||||
import { AppTheme, ProfileInfo, IconComponent } from '@backstage/plugin-api';
|
||||
import {
|
||||
AnyApiFactory,
|
||||
AppTheme,
|
||||
ProfileInfo,
|
||||
IconComponent,
|
||||
BackstagePlugin,
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
} from '@backstage/plugin-api';
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { SubRouteRef } from '../routing/types';
|
||||
import { AppIcons } from './icons';
|
||||
|
||||
export type BootErrorPageProps = {
|
||||
@@ -98,7 +103,9 @@ type PartialKeys<
|
||||
/**
|
||||
* Creates a map of target routes with matching parameters based on a map of external routes.
|
||||
*/
|
||||
type TargetRouteMap<ExternalRoutes extends AnyExternalRoutes> = {
|
||||
type TargetRouteMap<
|
||||
ExternalRoutes extends { [name: string]: ExternalRouteRef }
|
||||
> = {
|
||||
[name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef<
|
||||
infer Params,
|
||||
any
|
||||
@@ -107,7 +114,9 @@ type TargetRouteMap<ExternalRoutes extends AnyExternalRoutes> = {
|
||||
: never;
|
||||
};
|
||||
|
||||
export type AppRouteBinder = <ExternalRoutes extends AnyExternalRoutes>(
|
||||
export type AppRouteBinder = <
|
||||
ExternalRoutes extends { [name: string]: ExternalRouteRef }
|
||||
>(
|
||||
externalRoutes: ExternalRoutes,
|
||||
targetRoutes: PartialKeys<
|
||||
TargetRouteMap<ExternalRoutes>,
|
||||
@@ -215,21 +224,9 @@ export type BackstageApp = {
|
||||
* and any other components that should only be available while signed in.
|
||||
*/
|
||||
getRouter(): ComponentType<{}>;
|
||||
|
||||
/**
|
||||
* Routes component that contains all routes for plugin pages in the app.
|
||||
*
|
||||
* @deprecated Registering routes in plugins is deprecated and this method will be removed.
|
||||
*/
|
||||
getRoutes(): JSX.Element[];
|
||||
};
|
||||
|
||||
export type AppContext = {
|
||||
/**
|
||||
* @deprecated Will be removed
|
||||
*/
|
||||
getPlugins(): BackstagePlugin<any, any>[];
|
||||
|
||||
/**
|
||||
* Get a common or custom icon for this app.
|
||||
*/
|
||||
@@ -239,19 +236,4 @@ export type AppContext = {
|
||||
* Get the components registered for various purposes in the app.
|
||||
*/
|
||||
getComponents(): AppComponents;
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed
|
||||
*/
|
||||
getProvider(): ComponentType<{}>;
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed
|
||||
*/
|
||||
getRouter(): ComponentType<{}>;
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed
|
||||
*/
|
||||
getRoutes(): JSX.Element[];
|
||||
};
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { createPlugin } from '../plugin';
|
||||
import { createRouteRef } from '../routing';
|
||||
import { createPlugin, createRouteRef } from '@backstage/plugin-api';
|
||||
import { getComponentData } from './componentData';
|
||||
import {
|
||||
createComponentExtension,
|
||||
@@ -50,7 +49,7 @@ describe('extensions', () => {
|
||||
|
||||
it('should create react extensions of different types', () => {
|
||||
const Component = () => <div />;
|
||||
const routeRef = createRouteRef({ path: '/foo', title: 'Foo' });
|
||||
const routeRef = createRouteRef({ id: 'foo' });
|
||||
|
||||
const extension1 = createComponentExtension({
|
||||
component: {
|
||||
|
||||
@@ -15,9 +15,13 @@
|
||||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { RouteRef, useRouteRef } from '../routing';
|
||||
import { attachComponentData } from './componentData';
|
||||
import { Extension, BackstagePlugin } from '../plugin/types';
|
||||
import {
|
||||
Extension,
|
||||
BackstagePlugin,
|
||||
RouteRef,
|
||||
useRouteRef,
|
||||
} from '@backstage/plugin-api';
|
||||
|
||||
type ComponentLoader<T> =
|
||||
| {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import * as index from '.';
|
||||
|
||||
describe('index', () => {
|
||||
it('exports the plugin api', () => {
|
||||
it('exports the app api', () => {
|
||||
expect(index).toEqual({
|
||||
// Public API
|
||||
createApp: expect.any(Function),
|
||||
|
||||
@@ -16,7 +16,5 @@
|
||||
|
||||
export * from './apis';
|
||||
export * from './app';
|
||||
export * from './extensions';
|
||||
export * from './plugin';
|
||||
export * from './routing';
|
||||
export * from './types';
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
PluginConfig,
|
||||
PluginOutput,
|
||||
BackstagePlugin,
|
||||
Extension,
|
||||
AnyRoutes,
|
||||
AnyExternalRoutes,
|
||||
} from './types';
|
||||
import { AnyApiFactory } from '../apis';
|
||||
|
||||
export class PluginImpl<
|
||||
Routes extends AnyRoutes,
|
||||
ExternalRoutes extends AnyExternalRoutes
|
||||
> implements BackstagePlugin<Routes, ExternalRoutes> {
|
||||
private storedOutput?: PluginOutput[];
|
||||
|
||||
constructor(private readonly config: PluginConfig<Routes, ExternalRoutes>) {}
|
||||
|
||||
getId(): string {
|
||||
return this.config.id;
|
||||
}
|
||||
|
||||
getApis(): Iterable<AnyApiFactory> {
|
||||
return this.config.apis ?? [];
|
||||
}
|
||||
|
||||
get routes(): Routes {
|
||||
return this.config.routes ?? ({} as Routes);
|
||||
}
|
||||
|
||||
get externalRoutes(): ExternalRoutes {
|
||||
return this.config.externalRoutes ?? ({} as ExternalRoutes);
|
||||
}
|
||||
|
||||
output(): PluginOutput[] {
|
||||
if (this.storedOutput) {
|
||||
return this.storedOutput;
|
||||
}
|
||||
if (!this.config.register) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const outputs = new Array<PluginOutput>();
|
||||
|
||||
this.config.register({
|
||||
router: {
|
||||
addRoute(target, component, options) {
|
||||
outputs.push({
|
||||
type: 'route',
|
||||
target,
|
||||
component,
|
||||
options,
|
||||
});
|
||||
},
|
||||
},
|
||||
featureFlags: {
|
||||
register(name) {
|
||||
outputs.push({ type: 'feature-flag', name });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.storedOutput = outputs;
|
||||
return this.storedOutput;
|
||||
}
|
||||
|
||||
provide<T>(extension: Extension<T>): T {
|
||||
return extension.expose(this);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `plugin{${this.config.id}}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createPlugin<
|
||||
Routes extends AnyRoutes = {},
|
||||
ExternalRoutes extends AnyExternalRoutes = {}
|
||||
>(
|
||||
config: PluginConfig<Routes, ExternalRoutes>,
|
||||
): BackstagePlugin<Routes, ExternalRoutes> {
|
||||
return new PluginImpl(config);
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { PropsWithChildren } from 'react';
|
||||
import { createRouteRef } from '../routing';
|
||||
import { createPlugin } from './Plugin';
|
||||
import {
|
||||
createRoutableExtension,
|
||||
createComponentExtension,
|
||||
} from '../extensions';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
import {
|
||||
traverseElementTree,
|
||||
childDiscoverer,
|
||||
routeElementDiscoverer,
|
||||
} from '../extensions/traversal';
|
||||
import { pluginCollector } from './collectors';
|
||||
|
||||
const mockConfig = () => ({ path: '/foo', title: 'Foo' });
|
||||
const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
const pluginA = createPlugin({ id: 'my-plugin-a' });
|
||||
const pluginB = createPlugin({ id: 'my-plugin-b' });
|
||||
const pluginC = createPlugin({ id: 'my-plugin-c' });
|
||||
|
||||
const ref1 = createRouteRef(mockConfig());
|
||||
const ref2 = createRouteRef(mockConfig());
|
||||
|
||||
const Extension1 = pluginA.provide(
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref1,
|
||||
}),
|
||||
);
|
||||
const Extension2 = pluginB.provide(
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref2,
|
||||
}),
|
||||
);
|
||||
const Extension3 = pluginA.provide(
|
||||
createComponentExtension({ component: { sync: MockComponent } }),
|
||||
);
|
||||
const Extension4 = pluginB.provide(
|
||||
createComponentExtension({ component: { sync: MockComponent } }),
|
||||
);
|
||||
const Extension5 = pluginC.provide(
|
||||
createComponentExtension({ component: { sync: MockComponent } }),
|
||||
);
|
||||
|
||||
describe('collection', () => {
|
||||
it('should collect the plugins', () => {
|
||||
const root = (
|
||||
<MemoryRouter>
|
||||
<Routes>
|
||||
<Extension1 path="/foo">
|
||||
<div>
|
||||
<Extension2 path="/bar/:id">
|
||||
<div>
|
||||
<div />
|
||||
{[<Extension4 key={0} />]}
|
||||
Some text here shouldn't be a problem
|
||||
<div />
|
||||
{null}
|
||||
<div />
|
||||
<Extension3 />
|
||||
</div>
|
||||
</Extension2>
|
||||
{false}
|
||||
{true}
|
||||
{0}
|
||||
</div>
|
||||
</Extension1>
|
||||
<div>
|
||||
<Route path="/divsoup" element={<Extension5 />} />
|
||||
</div>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const { plugins } = traverseElementTree({
|
||||
root,
|
||||
discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
collectors: {
|
||||
plugins: pluginCollector,
|
||||
},
|
||||
});
|
||||
|
||||
expect(plugins).toEqual(new Set([pluginA, pluginB, pluginC]));
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 } from './types';
|
||||
import { getComponentData } from '../extensions';
|
||||
import { createCollector } from '../extensions/traversal';
|
||||
|
||||
export const pluginCollector = createCollector(
|
||||
() => new Set<BackstagePlugin<any, any>>(),
|
||||
(acc, node) => {
|
||||
const plugin = getComponentData<BackstagePlugin<any, any>>(
|
||||
node,
|
||||
'core.plugin',
|
||||
);
|
||||
if (plugin) {
|
||||
acc.add(plugin);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { createPlugin } from './Plugin';
|
||||
export type {
|
||||
BackstagePlugin,
|
||||
Extension,
|
||||
FeatureFlagOutput,
|
||||
FeatureFlagsHooks,
|
||||
LegacyRedirectRouteOutput,
|
||||
LegacyRouteOutput,
|
||||
PluginConfig,
|
||||
PluginHooks,
|
||||
PluginOutput,
|
||||
RedirectRouteOutput,
|
||||
RouteOptions,
|
||||
RouteOutput,
|
||||
RoutePath,
|
||||
RouterHooks,
|
||||
} from './types';
|
||||
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { ComponentType } from 'react';
|
||||
import { RouteRef, ExternalRouteRef } from '../routing';
|
||||
import { AnyApiFactory } from '../apis/system';
|
||||
|
||||
export type RouteOptions = {
|
||||
// Whether the route path must match exactly, defaults to true.
|
||||
exact?: boolean;
|
||||
};
|
||||
|
||||
export type RoutePath = string;
|
||||
|
||||
// Replace with using RouteRefs
|
||||
export type LegacyRouteOutput = {
|
||||
type: 'legacy-route';
|
||||
path: RoutePath;
|
||||
component: ComponentType<{}>;
|
||||
options?: RouteOptions;
|
||||
};
|
||||
|
||||
export type RouteOutput = {
|
||||
type: 'route';
|
||||
target: RouteRef;
|
||||
component: ComponentType<{}>;
|
||||
options?: RouteOptions;
|
||||
};
|
||||
|
||||
export type RedirectRouteOutput = {
|
||||
type: 'redirect-route';
|
||||
from: RouteRef;
|
||||
to: RouteRef;
|
||||
options?: RouteOptions;
|
||||
};
|
||||
|
||||
export type LegacyRedirectRouteOutput = {
|
||||
type: 'legacy-redirect-route';
|
||||
path: RoutePath;
|
||||
target: RoutePath;
|
||||
options?: RouteOptions;
|
||||
};
|
||||
|
||||
export type FeatureFlagOutput = {
|
||||
type: 'feature-flag';
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type PluginOutput =
|
||||
| LegacyRouteOutput
|
||||
| RouteOutput
|
||||
| LegacyRedirectRouteOutput
|
||||
| RedirectRouteOutput
|
||||
| FeatureFlagOutput;
|
||||
|
||||
export type Extension<T> = {
|
||||
expose(plugin: BackstagePlugin<any, any>): T;
|
||||
};
|
||||
|
||||
export type AnyRoutes = { [name: string]: RouteRef<any> };
|
||||
|
||||
export type AnyExternalRoutes = { [name: string]: ExternalRouteRef };
|
||||
|
||||
export type BackstagePlugin<
|
||||
Routes extends AnyRoutes = {},
|
||||
ExternalRoutes extends AnyExternalRoutes = {}
|
||||
> = {
|
||||
getId(): string;
|
||||
output(): PluginOutput[];
|
||||
getApis(): Iterable<AnyApiFactory>;
|
||||
provide<T>(extension: Extension<T>): T;
|
||||
routes: Routes;
|
||||
externalRoutes: ExternalRoutes;
|
||||
};
|
||||
|
||||
export type PluginConfig<
|
||||
Routes extends AnyRoutes,
|
||||
ExternalRoutes extends AnyExternalRoutes
|
||||
> = {
|
||||
id: string;
|
||||
apis?: Iterable<AnyApiFactory>;
|
||||
register?(hooks: PluginHooks): void;
|
||||
routes?: Routes;
|
||||
externalRoutes?: ExternalRoutes;
|
||||
};
|
||||
|
||||
export type PluginHooks = {
|
||||
/**
|
||||
* @deprecated All router hooks have been deprecated
|
||||
*/
|
||||
router: RouterHooks;
|
||||
featureFlags: FeatureFlagsHooks;
|
||||
};
|
||||
|
||||
export type RouterHooks = {
|
||||
/**
|
||||
* @deprecated Use a routable extension instead, see https://backstage.io/docs/plugins/composability#porting-existing-plugins
|
||||
*/
|
||||
addRoute(
|
||||
target: RouteRef,
|
||||
Component: ComponentType<any>,
|
||||
options?: RouteOptions,
|
||||
): void;
|
||||
};
|
||||
|
||||
export type FeatureFlagsHooks = {
|
||||
register(name: string): void;
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { AnyParams, ExternalRouteRef } from './types';
|
||||
import { createExternalRouteRef, isExternalRouteRef } from './ExternalRouteRef';
|
||||
import { isSubRouteRef } from './SubRouteRef';
|
||||
import { isRouteRef } from './RouteRef';
|
||||
|
||||
describe('ExternalRouteRef', () => {
|
||||
it('should be created', () => {
|
||||
const routeRef: ExternalRouteRef<undefined> = createExternalRouteRef({
|
||||
id: 'my-route-ref',
|
||||
});
|
||||
expect(routeRef.params).toEqual([]);
|
||||
expect(routeRef.optional).toBe(false);
|
||||
expect(String(routeRef)).toBe('routeRef{type=external,id=my-route-ref}');
|
||||
expect(isRouteRef(routeRef)).toBe(false);
|
||||
expect(isSubRouteRef(routeRef)).toBe(false);
|
||||
expect(isExternalRouteRef(routeRef)).toBe(true);
|
||||
|
||||
expect(isRouteRef({} as ExternalRouteRef)).toBe(false);
|
||||
});
|
||||
|
||||
it('should be created as optional', () => {
|
||||
const routeRef: ExternalRouteRef<{
|
||||
x: string;
|
||||
y: string;
|
||||
}> = createExternalRouteRef({
|
||||
id: 'my-other-route-ref',
|
||||
params: [],
|
||||
optional: true,
|
||||
});
|
||||
expect(routeRef.params).toEqual([]);
|
||||
expect(routeRef.optional).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be created with params', () => {
|
||||
const routeRef: ExternalRouteRef<{
|
||||
x: string;
|
||||
y: string;
|
||||
}> = createExternalRouteRef({
|
||||
id: 'my-other-route-ref',
|
||||
params: ['x', 'y'],
|
||||
});
|
||||
expect(routeRef.params).toEqual(['x', 'y']);
|
||||
expect(routeRef.optional).toEqual(false);
|
||||
});
|
||||
|
||||
it('should be created as optional with params', () => {
|
||||
const routeRef: ExternalRouteRef<{
|
||||
x: string;
|
||||
y: string;
|
||||
}> = createExternalRouteRef({
|
||||
id: 'my-other-route-ref',
|
||||
params: ['x', 'y'],
|
||||
optional: true,
|
||||
});
|
||||
expect(routeRef.params).toEqual(['x', 'y']);
|
||||
expect(routeRef.optional).toEqual(true);
|
||||
});
|
||||
|
||||
it('should properly infer and validate parameter types and assignments', () => {
|
||||
function validateType<T extends AnyParams, O extends boolean>(
|
||||
_ref: ExternalRouteRef<T, O>,
|
||||
) {}
|
||||
|
||||
const _1 = createExternalRouteRef({ id: '1', params: ['notX'] });
|
||||
// @ts-expect-error
|
||||
validateType<{ x: string }, any>(_1);
|
||||
validateType<{ notX: string }, any>(_1);
|
||||
|
||||
const _2 = createExternalRouteRef({
|
||||
id: '2',
|
||||
params: ['x'],
|
||||
optional: true,
|
||||
});
|
||||
// @ts-expect-error
|
||||
validateType<undefined, any>(_2);
|
||||
validateType<{ x: string }, true>(_2);
|
||||
|
||||
const _3 = createExternalRouteRef({ id: '3', params: ['x', 'y'] });
|
||||
// @ts-expect-error
|
||||
validateType<{ x: string }, any>(_3);
|
||||
// TODO(Rugvip): Ideally this would fail as well, but settle for validating it at runtime instead
|
||||
validateType<{ x: string; y: string; z: string }, any>(_3);
|
||||
validateType<{ x: string; y: string }, false>(_3);
|
||||
|
||||
const _4 = createExternalRouteRef({ id: '4', params: [] });
|
||||
// @ts-expect-error
|
||||
validateType<{ x: string }, any>(_4);
|
||||
validateType<undefined, false>(_4);
|
||||
|
||||
const _5 = createExternalRouteRef({ id: '5' });
|
||||
// @ts-expect-error
|
||||
validateType<{ x: string }, any>(_5);
|
||||
validateType<undefined, false>(_5);
|
||||
|
||||
const _6 = createExternalRouteRef({ id: '6', optional: true });
|
||||
// @ts-expect-error
|
||||
validateType<undefined, false>(_6);
|
||||
validateType<undefined, true>(_6);
|
||||
|
||||
// To avoid complains about missing expectations and unused vars
|
||||
expect([_1, _2, _3, _4, _5, _6].join('')).toEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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,
|
||||
routeRefType,
|
||||
AnyParams,
|
||||
ParamKeys,
|
||||
OptionalParams,
|
||||
} from './types';
|
||||
|
||||
export class ExternalRouteRefImpl<
|
||||
Params extends AnyParams,
|
||||
Optional extends boolean
|
||||
> implements ExternalRouteRef<Params, Optional> {
|
||||
readonly [routeRefType] = 'external';
|
||||
|
||||
constructor(
|
||||
private readonly id: string,
|
||||
readonly params: ParamKeys<Params>,
|
||||
readonly optional: Optional,
|
||||
) {}
|
||||
|
||||
toString() {
|
||||
return `routeRef{type=external,id=${this.id}}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createExternalRouteRef<
|
||||
Params extends { [param in ParamKey]: string },
|
||||
Optional extends boolean = false,
|
||||
ParamKey extends string = never
|
||||
>(options: {
|
||||
/**
|
||||
* An identifier for this route, used to identify it in error messages
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The parameters that will be provided to the external route reference.
|
||||
*/
|
||||
params?: ParamKey[];
|
||||
|
||||
/**
|
||||
* 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, `useRouteRef` will return `undefined`.
|
||||
*/
|
||||
optional?: Optional;
|
||||
}): ExternalRouteRef<OptionalParams<Params>, Optional> {
|
||||
return new ExternalRouteRefImpl(
|
||||
options.id,
|
||||
(options.params ?? []) as ParamKeys<OptionalParams<Params>>,
|
||||
Boolean(options.optional) as Optional,
|
||||
);
|
||||
}
|
||||
|
||||
export function isExternalRouteRef<
|
||||
Params extends AnyParams,
|
||||
Optional extends boolean
|
||||
>(
|
||||
routeRef:
|
||||
| RouteRef<Params>
|
||||
| SubRouteRef<Params>
|
||||
| ExternalRouteRef<Params, Optional>,
|
||||
): routeRef is ExternalRouteRef<Params, Optional> {
|
||||
return routeRef[routeRefType] === 'external';
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React, { ReactNode, Children, isValidElement, Fragment } from 'react';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
import { useApp } from '../app';
|
||||
import { useApp } from '@backstage/plugin-api';
|
||||
|
||||
type RouteObject = {
|
||||
path: string;
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { AnyParams, RouteRef } from './types';
|
||||
import { createRouteRef, isRouteRef } from './RouteRef';
|
||||
import { isSubRouteRef } from './SubRouteRef';
|
||||
import { isExternalRouteRef } from './ExternalRouteRef';
|
||||
import MyIcon from '@material-ui/icons/AcUnit';
|
||||
|
||||
describe('RouteRef', () => {
|
||||
it('should be created', () => {
|
||||
const routeRef: RouteRef<undefined> = createRouteRef({
|
||||
id: 'my-route-ref',
|
||||
});
|
||||
expect(routeRef.params).toEqual([]);
|
||||
expect(String(routeRef)).toBe('routeRef{type=absolute,id=my-route-ref}');
|
||||
expect(isRouteRef(routeRef)).toBe(true);
|
||||
expect(isSubRouteRef(routeRef)).toBe(false);
|
||||
expect(isExternalRouteRef(routeRef)).toBe(false);
|
||||
|
||||
expect(isRouteRef({} as RouteRef)).toBe(false);
|
||||
});
|
||||
|
||||
it('should be created with params', () => {
|
||||
const routeRef: RouteRef<{
|
||||
x: string;
|
||||
y: string;
|
||||
}> = createRouteRef({
|
||||
id: 'my-other-route-ref',
|
||||
params: ['x', 'y'],
|
||||
});
|
||||
expect(routeRef.params).toEqual(['x', 'y']);
|
||||
});
|
||||
|
||||
it('should properly infer and validate parameter types and assignments', () => {
|
||||
function validateType<T extends AnyParams>(_ref: RouteRef<T>) {}
|
||||
|
||||
const _1 = createRouteRef({ id: '1', params: ['x'] });
|
||||
// @ts-expect-error
|
||||
validateType<{ y: string }>(_1);
|
||||
// @ts-expect-error
|
||||
validateType<undefined>(_1);
|
||||
validateType<{ x: string }>(_1);
|
||||
|
||||
const _2 = createRouteRef({ id: '2', params: ['x', 'y'] });
|
||||
// @ts-expect-error
|
||||
validateType<{ x: string }>(_2);
|
||||
// @ts-expect-error
|
||||
validateType<undefined>(_2);
|
||||
// @ts-expect-error
|
||||
validateType<{ x: string; z: string }>(_2);
|
||||
// TODO(Rugvip): Ideally this would fail as well, but settle for validating it at runtime instead
|
||||
validateType<{ x: string; y: string; z: string }>(_2);
|
||||
validateType<{ x: string; y: string }>(_2);
|
||||
|
||||
const _3 = createRouteRef({ id: '3', params: [] });
|
||||
// @ts-expect-error
|
||||
validateType<{ x: string }>(_3);
|
||||
validateType<undefined>(_3);
|
||||
|
||||
const _4 = createRouteRef({ id: '4' });
|
||||
// @ts-expect-error
|
||||
validateType<{ x: string }>(_4);
|
||||
validateType<undefined>(_4);
|
||||
|
||||
// To avoid complains about missing expectations and unused vars
|
||||
expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String));
|
||||
});
|
||||
|
||||
it('should support deprecated access', () => {
|
||||
const routeRef = createRouteRef({
|
||||
title: 'My Ref',
|
||||
path: '/my-path',
|
||||
icon: MyIcon,
|
||||
});
|
||||
expect(routeRef.title).toBe('My Ref');
|
||||
expect(routeRef.path).toBe('/my-path');
|
||||
expect(routeRef.icon).toBe(MyIcon);
|
||||
expect(String(routeRef)).toBe('routeRef{type=absolute,id=My Ref}');
|
||||
});
|
||||
});
|
||||
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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,
|
||||
routeRefType,
|
||||
AnyParams,
|
||||
ParamKeys,
|
||||
OptionalParams,
|
||||
} from './types';
|
||||
import { IconComponent } from '@backstage/plugin-api';
|
||||
|
||||
// TODO(Rugvip): Remove this in the next breaking release, it's exported but unused
|
||||
export type RouteRefConfig<Params extends AnyParams> = {
|
||||
params?: ParamKeys<Params>;
|
||||
path?: string;
|
||||
icon?: IconComponent;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export class RouteRefImpl<Params extends AnyParams>
|
||||
implements RouteRef<Params> {
|
||||
readonly [routeRefType] = 'absolute';
|
||||
|
||||
constructor(
|
||||
private readonly id: string,
|
||||
readonly params: ParamKeys<Params>,
|
||||
private readonly config: {
|
||||
path?: string;
|
||||
icon?: IconComponent;
|
||||
title?: string;
|
||||
},
|
||||
) {}
|
||||
|
||||
get icon() {
|
||||
return this.config.icon;
|
||||
}
|
||||
|
||||
// TODO(Rugvip): Remove this, routes are looked up via the registry instead
|
||||
get path() {
|
||||
return this.config.path ?? '';
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.config.title ?? this.id;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `routeRef{type=absolute,id=${this.id}}`;
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
Params extends { [param in ParamKey]: string },
|
||||
// ParamKey is here to make sure the Params type properly has its keys narrowed down
|
||||
// to only the elements of params. Defaulting to never makes sure we end up with
|
||||
// Param = {} if the params array is empty.
|
||||
ParamKey extends string = never
|
||||
>(config: {
|
||||
/** The id of the route ref, used to identify it when printed */
|
||||
id?: string;
|
||||
/** A list of parameter names that the path that this route ref is bound to must contain */
|
||||
params?: ParamKey[];
|
||||
/** @deprecated Route refs no longer decide their own path */
|
||||
path?: string;
|
||||
/** @deprecated Route refs no longer decide their own icon */
|
||||
icon?: IconComponent;
|
||||
/** @deprecated Route refs no longer decide their own title */
|
||||
title?: string;
|
||||
}): RouteRef<OptionalParams<Params>> {
|
||||
const id = config.id || config.title;
|
||||
if (!id) {
|
||||
throw new Error('RouteRef must be provided a non-empty id');
|
||||
}
|
||||
return new RouteRefImpl(
|
||||
id,
|
||||
(config.params ?? []) as ParamKeys<OptionalParams<Params>>,
|
||||
config,
|
||||
);
|
||||
}
|
||||
|
||||
export function isRouteRef<Params extends AnyParams>(
|
||||
routeRef:
|
||||
| RouteRef<Params>
|
||||
| SubRouteRef<Params>
|
||||
| ExternalRouteRef<Params, any>,
|
||||
): routeRef is RouteRef<Params> {
|
||||
return routeRef[routeRefType] === 'absolute';
|
||||
}
|
||||
@@ -14,11 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createRouteRef } from './RouteRef';
|
||||
import { createSubRouteRef } from './SubRouteRef';
|
||||
import { createExternalRouteRef } from './ExternalRouteRef';
|
||||
import {
|
||||
createRouteRef,
|
||||
createSubRouteRef,
|
||||
createExternalRouteRef,
|
||||
ExternalRouteRef,
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
} from '@backstage/plugin-api';
|
||||
import { RouteResolver } from './RouteResolver';
|
||||
import { ExternalRouteRef, RouteRef, SubRouteRef } from './types';
|
||||
|
||||
const element = () => null;
|
||||
const rest = { element, caseSensitive: false };
|
||||
|
||||
@@ -18,16 +18,18 @@ import { generatePath, matchRoutes } from 'react-router-dom';
|
||||
import {
|
||||
AnyRouteRef,
|
||||
BackstageRouteObject,
|
||||
AnyParams,
|
||||
RouteFunc,
|
||||
routeRefType,
|
||||
} from './types';
|
||||
import {
|
||||
RouteRef,
|
||||
ExternalRouteRef,
|
||||
AnyParams,
|
||||
SubRouteRef,
|
||||
routeRefType,
|
||||
RouteFunc,
|
||||
} from './types';
|
||||
import { isRouteRef } from './RouteRef';
|
||||
import { isSubRouteRef } from './SubRouteRef';
|
||||
import { isExternalRouteRef } from './ExternalRouteRef';
|
||||
isRouteRef,
|
||||
isSubRouteRef,
|
||||
isExternalRouteRef,
|
||||
} from '@backstage/plugin-api';
|
||||
|
||||
// Joins a list of paths together, avoiding trailing and duplicate slashes
|
||||
function joinPaths(...paths: string[]): string {
|
||||
|
||||
+15
-18
@@ -31,38 +31,35 @@ import {
|
||||
routeElementDiscoverer,
|
||||
traverseElementTree,
|
||||
} from '../extensions/traversal';
|
||||
import { createPlugin } from '../plugin';
|
||||
import {
|
||||
createPlugin,
|
||||
useRouteRef,
|
||||
createRouteRef,
|
||||
createExternalRouteRef,
|
||||
RouteRef,
|
||||
ExternalRouteRef,
|
||||
} from '@backstage/plugin-api';
|
||||
import { RoutingProvider } from './RoutingProvider';
|
||||
import {
|
||||
routePathCollector,
|
||||
routeParentCollector,
|
||||
routeObjectCollector,
|
||||
} from './collectors';
|
||||
import { validateRoutes } from './validation';
|
||||
import { useRouteRef, RoutingProvider } from './hooks';
|
||||
import { createRouteRef, RouteRefConfig } from './RouteRef';
|
||||
import { RouteResolver } from './RouteResolver';
|
||||
import { createExternalRouteRef } from './ExternalRouteRef';
|
||||
import { AnyRouteRef, RouteFunc, RouteRef, ExternalRouteRef } from './types';
|
||||
import { AnyRouteRef, RouteFunc } from './types';
|
||||
|
||||
const mockConfig = (extra?: Partial<RouteRefConfig<{}>>) => ({
|
||||
path: '/unused',
|
||||
title: 'Unused',
|
||||
...extra,
|
||||
});
|
||||
const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
const plugin = createPlugin({ id: 'my-plugin' });
|
||||
|
||||
const ref1 = createRouteRef(mockConfig({ path: '/wat1' }));
|
||||
const ref2 = createRouteRef(mockConfig({ path: '/wat2' }));
|
||||
const ref3 = createRouteRef(mockConfig({ path: '/wat3' }));
|
||||
const ref4 = createRouteRef(mockConfig({ path: '/wat4' }));
|
||||
const ref5 = createRouteRef({
|
||||
...mockConfig({ path: '/wat5' }),
|
||||
params: ['x'],
|
||||
});
|
||||
const ref1 = createRouteRef({ id: 'ref1' });
|
||||
const ref2 = createRouteRef({ id: 'ref2' });
|
||||
const ref3 = createRouteRef({ id: 'ref3' });
|
||||
const ref4 = createRouteRef({ id: 'ref4' });
|
||||
const ref5 = createRouteRef({ id: 'ref5', params: ['x'] });
|
||||
const eRefA = createExternalRouteRef({ id: '1' });
|
||||
const eRefB = createExternalRouteRef({ id: '2' });
|
||||
const eRefC = createExternalRouteRef({ id: '3', params: ['y'] });
|
||||
+6
-59
@@ -14,74 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useMemo,
|
||||
Context,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import React, { createContext, ReactNode } from 'react';
|
||||
import { ExternalRouteRef, RouteRef, SubRouteRef } from '@backstage/plugin-api';
|
||||
import { getOrCreateGlobalSingleton } from '../lib/globalObject';
|
||||
import {
|
||||
BackstageRouteObject,
|
||||
RouteRef,
|
||||
ExternalRouteRef,
|
||||
AnyParams,
|
||||
SubRouteRef,
|
||||
RouteFunc,
|
||||
} from './types';
|
||||
import { RouteResolver } from './RouteResolver';
|
||||
import {
|
||||
VersionedValue,
|
||||
createVersionedValueMap,
|
||||
VersionedValue,
|
||||
} from '../lib/versionedValues';
|
||||
import {
|
||||
getGlobalSingleton,
|
||||
getOrCreateGlobalSingleton,
|
||||
} from '../lib/globalObject';
|
||||
import { RouteResolver } from './RouteResolver';
|
||||
import { BackstageRouteObject } from './types';
|
||||
|
||||
type RoutingContextType = VersionedValue<{ 1: RouteResolver }> | undefined;
|
||||
const RoutingContext = getOrCreateGlobalSingleton('routing-context', () =>
|
||||
createContext<RoutingContextType>(undefined),
|
||||
);
|
||||
|
||||
export function useRouteRef<Optional extends boolean, Params extends AnyParams>(
|
||||
routeRef: ExternalRouteRef<Params, Optional>,
|
||||
): Optional extends true ? RouteFunc<Params> | undefined : RouteFunc<Params>;
|
||||
export function useRouteRef<Params extends AnyParams>(
|
||||
routeRef: RouteRef<Params> | SubRouteRef<Params>,
|
||||
): RouteFunc<Params>;
|
||||
export function useRouteRef<Params extends AnyParams>(
|
||||
routeRef:
|
||||
| RouteRef<Params>
|
||||
| SubRouteRef<Params>
|
||||
| ExternalRouteRef<Params, any>,
|
||||
): RouteFunc<Params> | undefined {
|
||||
const sourceLocation = useLocation();
|
||||
const versionedContext = useContext(
|
||||
getGlobalSingleton<Context<RoutingContextType>>('routing-context'),
|
||||
);
|
||||
const resolver = versionedContext?.atVersion(1);
|
||||
const routeFunc = useMemo(
|
||||
() => resolver && resolver.resolve(routeRef, sourceLocation),
|
||||
[resolver, routeRef, sourceLocation],
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
type ProviderProps = {
|
||||
routePaths: Map<RouteRef, string>;
|
||||
routeParents: Map<RouteRef, RouteRef | undefined>;
|
||||
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { AnyParams, SubRouteRef } from './types';
|
||||
import { createSubRouteRef, isSubRouteRef } from './SubRouteRef';
|
||||
import { createRouteRef, isRouteRef } from './RouteRef';
|
||||
import { isExternalRouteRef } from './ExternalRouteRef';
|
||||
|
||||
const parent = createRouteRef({ id: 'parent' });
|
||||
const parentX = createRouteRef({ id: 'parent-x', params: ['x'] });
|
||||
|
||||
describe('SubRouteRef', () => {
|
||||
it('should be created', () => {
|
||||
const routeRef: SubRouteRef<undefined> = createSubRouteRef({
|
||||
parent,
|
||||
id: 'my-route-ref',
|
||||
path: '/foo',
|
||||
});
|
||||
expect(routeRef.path).toBe('/foo');
|
||||
expect(routeRef.parent).toBe(parent);
|
||||
expect(routeRef.params).toEqual([]);
|
||||
expect(String(routeRef)).toBe('routeRef{type=sub,id=my-route-ref}');
|
||||
expect(isRouteRef(routeRef)).toBe(false);
|
||||
expect(isSubRouteRef(routeRef)).toBe(true);
|
||||
expect(isExternalRouteRef(routeRef)).toBe(false);
|
||||
|
||||
expect(isRouteRef({} as SubRouteRef)).toBe(false);
|
||||
});
|
||||
|
||||
it('should be created with params', () => {
|
||||
const routeRef: SubRouteRef<{ bar: string }> = createSubRouteRef({
|
||||
parent,
|
||||
id: 'my-other-route-ref',
|
||||
path: '/foo/:bar',
|
||||
});
|
||||
expect(routeRef.path).toBe('/foo/:bar');
|
||||
expect(routeRef.parent).toBe(parent);
|
||||
expect(routeRef.params).toEqual(['bar']);
|
||||
});
|
||||
|
||||
it('should be created with merged params', () => {
|
||||
const routeRef: SubRouteRef<{
|
||||
x: string;
|
||||
y: string;
|
||||
z: string;
|
||||
}> = createSubRouteRef({
|
||||
parent: parentX,
|
||||
id: 'my-other-route-ref',
|
||||
path: '/foo/:y/:z',
|
||||
});
|
||||
expect(routeRef.path).toBe('/foo/:y/:z');
|
||||
expect(routeRef.parent).toBe(parentX);
|
||||
expect(routeRef.params).toEqual(['x', 'y', 'z']);
|
||||
});
|
||||
|
||||
it('should be created with params from parent', () => {
|
||||
const routeRef: SubRouteRef<{ x: string }> = createSubRouteRef({
|
||||
parent: parentX,
|
||||
id: 'my-other-route-ref',
|
||||
path: '/foo/bar',
|
||||
});
|
||||
expect(routeRef.path).toBe('/foo/bar');
|
||||
expect(routeRef.parent).toBe(parentX);
|
||||
expect(routeRef.params).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, id: path }),
|
||||
).toThrow(message);
|
||||
});
|
||||
|
||||
it('should properly infer and parse path parameters', () => {
|
||||
function validateType<T extends AnyParams>(_ref: SubRouteRef<T>) {}
|
||||
|
||||
const _1 = createSubRouteRef({ id: '1', parent, path: '/foo/bar' });
|
||||
// @ts-expect-error
|
||||
validateType<{ x: string }>(_1);
|
||||
validateType<undefined>(_1);
|
||||
|
||||
const _2 = createSubRouteRef({ id: '2', parent, path: '/foo/:x/:y' });
|
||||
// @ts-expect-error
|
||||
validateType<undefined>(_2);
|
||||
// @ts-expect-error
|
||||
validateType<{ x: string; z: string }>(_2);
|
||||
// @ts-expect-error
|
||||
validateType<{ y: string }>(_2);
|
||||
// TODO(Rugvip): Ideally this would fail as well, but settle for validating it at runtime instead
|
||||
validateType<{ x: string; y: string; z: string }>(_2);
|
||||
validateType<{ x: string; y: string }>(_2);
|
||||
|
||||
const _3 = createSubRouteRef({ id: '3', parent: parentX, path: '/foo' });
|
||||
// @ts-expect-error
|
||||
validateType<undefined>(_3);
|
||||
// @ts-expect-error
|
||||
validateType<{ y: string }>(_3);
|
||||
validateType<{ x: string }>(_3);
|
||||
|
||||
const _4 = createSubRouteRef({ id: '4', parent: parentX, path: '/foo/:y' });
|
||||
// @ts-expect-error
|
||||
validateType<undefined>(_4);
|
||||
// @ts-expect-error
|
||||
validateType<{ x: string; z: string }>(_4);
|
||||
// @ts-expect-error
|
||||
validateType<{ y: string }>(_4);
|
||||
validateType<{ x: string; y: string }>(_4);
|
||||
|
||||
// To avoid complains about missing expectations and unused vars
|
||||
expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
AnyParams,
|
||||
ExternalRouteRef,
|
||||
OptionalParams,
|
||||
ParamKeys,
|
||||
RouteRef,
|
||||
routeRefType,
|
||||
SubRouteRef,
|
||||
} from './types';
|
||||
|
||||
// Should match the pattern in react-router
|
||||
const PARAM_PATTERN = /^\w+$/;
|
||||
|
||||
export class SubRouteRefImpl<Params extends AnyParams>
|
||||
implements SubRouteRef<Params> {
|
||||
readonly [routeRefType] = 'sub';
|
||||
|
||||
constructor(
|
||||
private readonly id: string,
|
||||
readonly path: string,
|
||||
readonly parent: RouteRef,
|
||||
readonly params: ParamKeys<Params>,
|
||||
) {}
|
||||
|
||||
toString() {
|
||||
return `routeRef{type=sub,id=${this.id}}`;
|
||||
}
|
||||
}
|
||||
|
||||
// These utility types help us infer a Param object type from a string path
|
||||
// For example, `/foo/:bar/:baz` inferred to `{ bar: string, baz: string }`
|
||||
type ParamPart<S extends string> = S extends `:${infer Param}` ? Param : never;
|
||||
type ParamNames<S extends string> = S extends `${infer Part}/${infer Rest}`
|
||||
? ParamPart<Part> | ParamNames<Rest>
|
||||
: ParamPart<S>;
|
||||
type PathParams<S extends string> = { [name in ParamNames<S>]: string };
|
||||
|
||||
/**
|
||||
* Merges a param object type with with an optional params type into a params object
|
||||
*/
|
||||
type MergeParams<
|
||||
P1 extends { [param in string]: string },
|
||||
P2 extends AnyParams
|
||||
> = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
type MakeSubRouteRef<
|
||||
Params extends { [param in string]: string },
|
||||
ParentParams extends AnyParams
|
||||
> = keyof Params & keyof ParentParams extends never
|
||||
? SubRouteRef<OptionalParams<MergeParams<Params, ParentParams>>>
|
||||
: never;
|
||||
|
||||
export function createSubRouteRef<
|
||||
Path extends string,
|
||||
ParentParams extends AnyParams = never
|
||||
>(config: {
|
||||
id: string;
|
||||
path: Path;
|
||||
parent: RouteRef<ParentParams>;
|
||||
}): MakeSubRouteRef<PathParams<Path>, ParentParams> {
|
||||
const { id, path, parent } = config;
|
||||
type Params = PathParams<Path>;
|
||||
|
||||
// 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 = [...parent.params, ...pathParams];
|
||||
|
||||
if (parent.params.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(
|
||||
id,
|
||||
path,
|
||||
parent,
|
||||
params as ParamKeys<MergeParams<Params, ParentParams>>,
|
||||
) as SubRouteRef<OptionalParams<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;
|
||||
}
|
||||
|
||||
export function isSubRouteRef<Params extends AnyParams>(
|
||||
routeRef:
|
||||
| RouteRef<Params>
|
||||
| SubRouteRef<Params>
|
||||
| ExternalRouteRef<Params, any>,
|
||||
): routeRef is SubRouteRef<Params> {
|
||||
return routeRef[routeRefType] === 'sub';
|
||||
}
|
||||
@@ -26,11 +26,9 @@ import {
|
||||
childDiscoverer,
|
||||
routeElementDiscoverer,
|
||||
} from '../extensions/traversal';
|
||||
import { createRouteRef } from './RouteRef';
|
||||
import { createPlugin } from '../plugin';
|
||||
import { createRouteRef, createPlugin, RouteRef } from '@backstage/plugin-api';
|
||||
import { attachComponentData, createRoutableExtension } from '../extensions';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
import { RouteRef } from './types';
|
||||
|
||||
const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => (
|
||||
<>{children}</>
|
||||
@@ -38,11 +36,11 @@ const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => (
|
||||
|
||||
const plugin = createPlugin({ id: 'my-plugin' });
|
||||
|
||||
const ref1 = createRouteRef({ path: '/foo1', title: 'Foo' });
|
||||
const ref2 = createRouteRef({ path: '/foo2', title: 'Foo' });
|
||||
const ref3 = createRouteRef({ path: '/foo3', title: 'Foo' });
|
||||
const ref4 = createRouteRef({ path: '/foo4', title: 'Foo' });
|
||||
const ref5 = createRouteRef({ path: '/foo5', title: 'Foo' });
|
||||
const ref1 = createRouteRef({ id: 'ref1' });
|
||||
const ref2 = createRouteRef({ id: 'ref2' });
|
||||
const ref3 = createRouteRef({ id: 'ref3' });
|
||||
const ref4 = createRouteRef({ id: 'ref4' });
|
||||
const ref5 = createRouteRef({ id: 'ref5' });
|
||||
const refOrder = [ref1, ref2, ref3, ref4, ref5];
|
||||
|
||||
const Extension1 = plugin.provide(
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
*/
|
||||
|
||||
import { isValidElement, ReactElement, ReactNode } from 'react';
|
||||
import { BackstageRouteObject, RouteRef } from '../routing/types';
|
||||
import { RouteRef } from '@backstage/plugin-api';
|
||||
import { BackstageRouteObject } from '../routing/types';
|
||||
import { getComponentData } from '../extensions';
|
||||
import { createCollector } from '../extensions/traversal';
|
||||
|
||||
|
||||
@@ -14,16 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type {
|
||||
RouteRef,
|
||||
AbsoluteRouteRef,
|
||||
ConcreteRoute,
|
||||
MutableRouteRef,
|
||||
ExternalRouteRef,
|
||||
} from './types';
|
||||
export { FlatRoutes } from './FlatRoutes';
|
||||
export { createRouteRef } from './RouteRef';
|
||||
export { createSubRouteRef } from './SubRouteRef';
|
||||
export { createExternalRouteRef } from './ExternalRouteRef';
|
||||
export type { RouteRefConfig } from './RouteRef';
|
||||
export { useRouteRef } from './hooks';
|
||||
|
||||
@@ -14,16 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IconComponent } from '@backstage/plugin-api';
|
||||
import { RouteRef, SubRouteRef, ExternalRouteRef } from '@backstage/plugin-api';
|
||||
import { getOrCreateGlobalSingleton } from '../lib/globalObject';
|
||||
|
||||
export const routeRefType: unique symbol = getOrCreateGlobalSingleton<any>(
|
||||
'route-ref-type',
|
||||
() => Symbol('route-ref-type'),
|
||||
);
|
||||
|
||||
export type AnyParams = { [param in string]: string } | undefined;
|
||||
export type ParamKeys<Params extends AnyParams> = keyof Params extends never
|
||||
? []
|
||||
: (keyof Params)[];
|
||||
export type OptionalParams<
|
||||
Params extends { [param in string]: string }
|
||||
> = Params[keyof Params] extends never ? undefined : Params;
|
||||
|
||||
export type AnyRouteRef =
|
||||
| RouteRef<any>
|
||||
| SubRouteRef<any>
|
||||
| ExternalRouteRef<any, any>;
|
||||
|
||||
// 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.
|
||||
@@ -34,59 +38,6 @@ export type RouteFunc<Params extends AnyParams> = (
|
||||
...[params]: Params extends undefined ? readonly [] : readonly [Params]
|
||||
) => string;
|
||||
|
||||
export const routeRefType: unique symbol = getOrCreateGlobalSingleton<any>(
|
||||
'route-ref-type',
|
||||
() => Symbol('route-ref-type'),
|
||||
);
|
||||
|
||||
export type RouteRef<Params extends AnyParams = any> = {
|
||||
readonly [routeRefType]: 'absolute';
|
||||
|
||||
params: ParamKeys<Params>;
|
||||
|
||||
// TODO(Rugvip): Remove all of these once plugins don't rely on the path
|
||||
/** @deprecated paths are no longer accessed directly from RouteRefs, use useRouteRef instead */
|
||||
path: string;
|
||||
/** @deprecated icons are no longer accessed via RouteRefs */
|
||||
icon?: IconComponent;
|
||||
/** @deprecated titles are no longer accessed via RouteRefs */
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type SubRouteRef<Params extends AnyParams = any> = {
|
||||
readonly [routeRefType]: 'sub';
|
||||
|
||||
parent: RouteRef;
|
||||
|
||||
path: string;
|
||||
|
||||
params: ParamKeys<Params>;
|
||||
};
|
||||
|
||||
export type ExternalRouteRef<
|
||||
Params extends AnyParams = any,
|
||||
Optional extends boolean = any
|
||||
> = {
|
||||
readonly [routeRefType]: 'external';
|
||||
|
||||
params: ParamKeys<Params>;
|
||||
|
||||
optional?: Optional;
|
||||
};
|
||||
|
||||
export type AnyRouteRef =
|
||||
| RouteRef<any>
|
||||
| SubRouteRef<any>
|
||||
| ExternalRouteRef<any, any>;
|
||||
|
||||
// TODO(Rugvip): None of these should be found in the wild anymore, remove in next minor release
|
||||
/** @deprecated */
|
||||
export type ConcreteRoute = {};
|
||||
/** @deprecated */
|
||||
export type AbsoluteRouteRef = RouteRef<{}>;
|
||||
/** @deprecated */
|
||||
export type MutableRouteRef = RouteRef<{}>;
|
||||
|
||||
// A duplicate of the react-router RouteObject, but with routeRef added
|
||||
export interface BackstageRouteObject {
|
||||
caseSensitive: boolean;
|
||||
|
||||
Reference in New Issue
Block a user