Merge pull request #24777 from backstage/rugvip/binding-backport

core-*-api: backport support for external route ref default targets + config route binding
This commit is contained in:
Patrik Oldsberg
2024-06-04 16:00:56 +02:00
committed by GitHub
27 changed files with 525 additions and 114 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-api-docs': patch
---
The `registerComponent` external route will now by default bind to the catalog import page if it is available.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-compat-api': patch
---
Add support for forwarding default target from legacy external route refs.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
The `catalogIndex` external route is now optional and will by default bind to the catalog index page if it is available.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': patch
---
Added a new `defaultTarget` option to `createExternalRouteRef`. I lets you specify a default target of the route by name, for example `'catalog.catalogIndex'`, which will be used if the target route is present in the app and there is no explicit route binding.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-graph': patch
---
The `catalogEntity` external route will now by default bind to the catalog entity page if it is available.
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/plugin-catalog': minor
---
Added the following default targets for external routes:
- `createComponent` binds to the Scaffolder page.
- `viewTechDoc` binds to the TechDocs entity documentation page.
- `createFromTemplate` binds to the Scaffolder selected template page.
+29
View File
@@ -0,0 +1,29 @@
---
'@backstage/core-app-api': patch
---
Added support for configuration of route bindings through static configuration, and default targets for external route refs.
In addition to configuring route bindings through code, it is now also possible to configure route bindings under the `app.routes.bindings` key, for example:
```yaml
app:
routes:
bindings:
catalog.createComponent: catalog-import.importPage
```
Each key in the route binding object is of the form `<plugin-id>.<externalRouteName>`, where the route name is key used in the `externalRoutes` object passed to `createPlugin`. The value is of the same form, but with the name taken from the plugin `routes` option instead.
The equivalent of the above configuration in code is the following:
```ts
const app = createApp({
// ...
bindRoutes({ bind }) {
bind(catalogPlugin.externalRoutes, {
createComponent: catalogImportPlugin.routes.importPage,
});
},
});
```
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-scaffolder': minor
---
Added the following default targets for external routes:
- `registerComponent` binds to the catalog import page.
- `viewTechDoc` binds to the TechDocs entity documentation page.
@@ -274,6 +274,21 @@ Note that we are not importing and using the `RouteRef`s directly in the app, an
Another thing to note is that this indirection in the routing is particularly useful for open source plugins that need to provide flexibility in how they are integrated. For plugins that you build internally for your own Backstage application, you can choose to use direct imports or even concrete route path strings directly. Although there can be some benefits to using the full routing system even in internal plugins: it can help you structure your routes, and as you will see further down it also helps you manage route parameters.
### Default Targets for External Route References
It is possible to define a default target for an external route reference, potentially removing the need to bind the route in the app. This reduces the need for configuration when installing new plugins through providing a sensible default. It is of course still possible to override the route binding in the app.
The default target uses the same syntax as the route binding configuration, and will only be used if the target plugin and route exist. For example, this is how the catalog can define a default target for the create component external route in a way that removes the need for the binding in the previous example:
```tsx title="plugins/catalog/src/routes.ts"
import { createExternalRouteRef } from '@backstage/frontend-plugin-api';
export const createComponentExternalRouteRef = createExternalRouteRef({
// highlight-next-line
defaultTarget: 'scaffolder.createComponent',
});
```
### Optional External Route References
It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app.
+27
View File
@@ -325,6 +325,33 @@ concrete routes directly. Although there can be some benefits to using the full
routing system even in internal plugins. It can help you structure your routes,
and as you will see further down it also helps you manage route parameters.
You can also use static configuration to bind routes, removing the need to make
changes to the app code. It does however mean that you won't get type safety
when binding routes and compile-time validation of the bindings. Static
configuration of route bindings is done under the `app.routes.bindings` key in
`app-config.yaml`. It works the same way as [route bindings in the new frontend system](../frontend-system/architecture/07-routes.md#binding-external-route-references),
for example:
```yaml
app:
routes:
bindings:
bar.headerLink: foo.root
```
### Default Targets for External Route References
Following the `1.28` release of Backstage you can now define default targets for
external route references. They work the same way as [default targets in the new frontend system](../frontend-system/architecture/07-routes.md#default-targets-for-external-route-references),
for example:
```ts
export const createComponentExternalRouteRef = createExternalRouteRef({
// highlight-next-line
defaultTarget: 'scaffolder.createComponent',
});
```
### Optional External Routes
When creating an `ExternalRouteRef` it is possible to mark it as optional:
+4 -30
View File
@@ -33,22 +33,14 @@ import {
OAuthRequestDialog,
SignInPage,
} from '@backstage/core-components';
import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
import {
CatalogEntityPage,
CatalogIndexPage,
catalogPlugin,
} from '@backstage/plugin-catalog';
import { ApiExplorerPage } from '@backstage/plugin-api-docs';
import { CatalogEntityPage, CatalogIndexPage } from '@backstage/plugin-catalog';
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import {
CatalogImportPage,
catalogImportPlugin,
} from '@backstage/plugin-catalog-import';
import { orgPlugin } from '@backstage/plugin-org';
import { CatalogImportPage } from '@backstage/plugin-catalog-import';
import { HomepageCompositionRoot, VisitListener } from '@backstage/plugin-home';
import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { ScaffolderPage } from '@backstage/plugin-scaffolder';
import {
ScaffolderFieldExtensions,
ScaffolderLayouts,
@@ -56,7 +48,6 @@ import {
import { SearchPage } from '@backstage/plugin-search';
import {
TechDocsIndexPage,
techdocsPlugin,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
@@ -119,23 +110,6 @@ const app = createApp({
);
},
},
bindRoutes({ bind }) {
bind(catalogPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
viewTechDoc: techdocsPlugin.routes.docRoot,
createFromTemplate: scaffolderPlugin.routes.selectedTemplate,
});
bind(apiDocsPlugin.externalRoutes, {
registerApi: catalogImportPlugin.routes.importPage,
});
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
viewTechDoc: techdocsPlugin.routes.docRoot,
});
bind(orgPlugin.externalRoutes, {
catalogIndex: catalogPlugin.routes.catalogIndex,
});
},
});
const routes = (
@@ -20,7 +20,7 @@ import {
renderWithEffects,
withLogCollector,
} from '@backstage/test-utils';
import { render, screen, waitFor, act } from '@testing-library/react';
import { screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { PropsWithChildren, ReactNode } from 'react';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
@@ -624,7 +624,7 @@ describe('Integration Test', () => {
expect(capturedEvents).toHaveLength(2);
});
it('should throw some error when the route has duplicate params', () => {
it('should throw some error when the route has duplicate params', async () => {
const app = new AppManager({
apis: [],
defaultApis: [],
@@ -641,31 +641,43 @@ describe('Integration Test', () => {
},
});
const expectedMessage =
'Parameter :thing is duplicated in path test/:thing/some/:thing';
const Provider = app.getProvider();
const Router = app.getRouter();
const { error: errorLogs } = withLogCollector(() => {
render(
<Provider>
<Router>
<Routes>
<Route path="/test/:thing" element={<ExposedComponent />}>
<Route path="/some/:thing" element={<HiddenComponent />} />
</Route>
</Routes>
</Router>
</Provider>,
);
const { error: errorLogs } = await withLogCollector(async () => {
await expect(() =>
renderWithEffects(
<Provider>
<Router>
<Routes>
<Route path="/test/:thing" element={<ExposedComponent />}>
<Route path="/some/:thing" element={<HiddenComponent />} />
</Route>
</Routes>
</Router>
</Provider>,
),
).rejects.toThrow(expectedMessage);
});
expect(errorLogs).toEqual([
expect.objectContaining({
message: expect.stringContaining(
'Parameter :thing is duplicated in path test/:thing/some/:thing',
),
detail: new Error(expectedMessage),
type: 'unhandled exception',
}),
expect.objectContaining({
detail: new Error(expectedMessage),
type: 'unhandled exception',
}),
expect.stringContaining(
'The above error occurred in the <Provider> component:',
),
]);
});
it('should throw an error when required external plugin routes are not bound', () => {
it('should throw an error when required external plugin routes are not bound', async () => {
const app = new AppManager({
apis: [],
defaultApis: [],
@@ -676,25 +688,36 @@ describe('Integration Test', () => {
configLoader: async () => [],
});
const expectedMessage =
"External route 'extRouteRef1' of the 'blob' plugin must be bound to a target route. See https://backstage.io/link?bind-routes for details.";
const Provider = app.getProvider();
const Router = app.getRouter();
const { error: errorLogs } = withLogCollector(() => {
render(
<Provider>
<Router>
<Routes>
<Route path="/test/:thing" element={<ExposedComponent />} />
</Routes>
</Router>
</Provider>,
);
const { error: errorLogs } = await withLogCollector(async () => {
await expect(() =>
renderWithEffects(
<Provider>
<Router>
<Routes>
<Route path="/test/:thing" element={<ExposedComponent />} />
</Routes>
</Router>
</Provider>,
),
).rejects.toThrow(expectedMessage);
});
expect(errorLogs).toEqual([
expect.objectContaining({
message: expect.stringMatching(
/^External route 'extRouteRef1' of the 'blob' plugin must be bound to a target route/,
),
detail: new Error(expectedMessage),
type: 'unhandled exception',
}),
expect.objectContaining({
detail: new Error(expectedMessage),
type: 'unhandled exception',
}),
expect.stringContaining(
'The above error occurred in the <Provider> component:',
),
]);
});
+24 -16
View File
@@ -233,8 +233,10 @@ export class AppManager implements BackstageApp {
const appContext = new AppContextImpl(this);
// We only validate routes once
let routesHaveBeenValidated = false;
// We only bind and validate routes once
let routeBindings: ReturnType<typeof resolveRouteBindings>;
// Store and keep throwing the same error if we encounter one
let routeValidationError: Error | undefined = undefined;
const Provider = ({ children }: PropsWithChildren<{}>) => {
const needsFeatureFlagRegistrationRef = useRef(true);
@@ -243,7 +245,7 @@ export class AppManager implements BackstageApp {
[],
);
const { routing, featureFlags, routeBindings } = useMemo(() => {
const { routing, featureFlags } = useMemo(() => {
const usesReactRouterBeta = isReactRouterBeta();
if (usesReactRouterBeta) {
// eslint-disable-next-line no-console
@@ -275,21 +277,9 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
// Initialize APIs once all plugins are available
this.getApiHolder();
return {
...result,
routeBindings: resolveRouteBindings(this.bindRoutes),
};
return result;
}, [children]);
if (!routesHaveBeenValidated) {
routesHaveBeenValidated = true;
validateRouteParameters(routing.paths, routing.parents);
validateRouteBindings(
routeBindings,
this.plugins as Iterable<BackstagePlugin>,
);
}
const loadedConfig = useConfigLoader(
this.configLoader,
this.components,
@@ -307,6 +297,24 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
return loadedConfig.node;
}
if (routeValidationError) {
throw routeValidationError;
} else if (!routeBindings) {
try {
routeBindings = resolveRouteBindings(
this.bindRoutes,
loadedConfig.api,
this.plugins,
);
validateRouteParameters(routing.paths, routing.parents);
validateRouteBindings(routeBindings, this.plugins);
} catch (error) {
routeValidationError = error;
throw error;
}
}
// We can't register feature flags just after the element traversal, because the
// config API isn't available yet and implementations frequently depend on it.
// Instead we make it happen immediately, to make sure all flags are available
@@ -16,17 +16,23 @@
import {
createExternalRouteRef,
createPlugin,
createRouteRef,
} from '@backstage/core-plugin-api';
import { resolveRouteBindings } from './resolveRouteBindings';
import { collectRouteIds, resolveRouteBindings } from './resolveRouteBindings';
import { MockConfigApi } from '@backstage/test-utils';
describe('resolveRouteBindings', () => {
it('runs happy path', () => {
const external = { myRoute: createExternalRouteRef({ id: '1' }) };
const ref = createRouteRef({ id: 'ref-1' });
const result = resolveRouteBindings(({ bind }) => {
bind(external, { myRoute: ref });
});
const result = resolveRouteBindings(
({ bind }) => {
bind(external, { myRoute: ref });
},
new MockConfigApi({}),
[],
);
expect(result.get(external.myRoute)).toBe(ref);
});
@@ -35,9 +41,157 @@ describe('resolveRouteBindings', () => {
const external = { myRoute: createExternalRouteRef({ id: '2' }) };
const ref = createRouteRef({ id: 'ref-2' });
expect(() =>
resolveRouteBindings(({ bind }) => {
bind(external, { someOtherRoute: ref } as any);
}),
resolveRouteBindings(
({ bind }) => {
bind(external, { someOtherRoute: ref } as any);
},
new MockConfigApi({}),
[],
),
).toThrow('Key someOtherRoute is not an existing external route');
});
it('reads bindings from config', () => {
const mySource = createExternalRouteRef({ id: 'test' });
const myTarget = createRouteRef({ id: 'test' });
const result = resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } },
}),
[
createPlugin({
id: 'test',
routes: {
myTarget,
},
externalRoutes: {
mySource,
},
}),
],
);
expect(result.get(mySource)).toBe(myTarget);
});
it('throws on invalid config', () => {
expect(() =>
resolveRouteBindings(
() => {},
new MockConfigApi({ app: { routes: { bindings: 'derp' } } }),
[],
),
).toThrow(
"Invalid type in config for key 'app.routes.bindings' in 'mock-config', got string, wanted object",
);
expect(() =>
resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': true } } },
}),
[],
),
).toThrow(
"Invalid config at app.routes.bindings['test.mySource'], value must be a non-empty string",
);
expect(() =>
resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } },
}),
[],
),
).toThrow(
"Invalid config at app.routes.bindings, 'test.mySource' is not a valid external route",
);
expect(() =>
resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } },
}),
[
createPlugin({
id: 'test',
externalRoutes: {
mySource: createExternalRouteRef({ id: 'test' }),
},
}),
],
),
).toThrow(
"Invalid config at app.routes.bindings['test.mySource'], 'test.myTarget' is not a valid route",
);
});
it('can have default targets, but at the lowest priority', () => {
const source = createExternalRouteRef({
id: 'test',
defaultTarget: 'test.target1',
});
const target1 = createRouteRef({ id: 'test' });
const target2 = createRouteRef({ id: 'test' });
const plugin = createPlugin({
id: 'test',
routes: {
target1,
target2,
},
externalRoutes: {
source,
},
});
// defaultTarget wins only if no bind or config matches
let result = resolveRouteBindings(() => {}, new MockConfigApi({}), [
plugin,
]);
expect(result.get(source)).toBe(target1);
// config wins over defaultTarget
result = resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.source': 'test.target2' } } },
}),
[plugin],
);
expect(result.get(source)).toBe(target2);
// bind wins over defaultTarget
result = resolveRouteBindings(
({ bind }) => {
bind(plugin.externalRoutes, { source: plugin.routes.target2 });
},
new MockConfigApi({}),
[plugin],
);
expect(result.get(source)).toBe(target2);
});
});
describe('collectRouteIds', () => {
it('should assign IDs to routes', () => {
const ref = createRouteRef({ id: 'ignored' });
const extRef = createExternalRouteRef({ id: 'ignored' });
const collected = collectRouteIds([
createPlugin({ id: 'test', routes: { ref }, externalRoutes: { extRef } }),
]);
expect(Object.fromEntries(collected.routes)).toEqual({
'test.ref': ref,
});
expect(Object.fromEntries(collected.externalRoutes)).toEqual({
'test.extRef': extRef,
});
});
});
@@ -18,12 +18,63 @@ import {
RouteRef,
SubRouteRef,
ExternalRouteRef,
BackstagePlugin,
AnyRoutes,
AnyExternalRoutes,
} from '@backstage/core-plugin-api';
import { AppOptions, AppRouteBinder } from './types';
import { Config } from '@backstage/config';
import { JsonObject } from '@backstage/types';
export function resolveRouteBindings(bindRoutes: AppOptions['bindRoutes']) {
/** @internal */
export function collectRouteIds(
plugins: Iterable<
Pick<
BackstagePlugin<AnyRoutes, AnyExternalRoutes>,
'getId' | 'routes' | 'externalRoutes'
>
>,
) {
const routesById = new Map<string, RouteRef | SubRouteRef>();
const externalRoutesById = new Map<string, ExternalRouteRef>();
for (const plugin of plugins) {
for (const [name, ref] of Object.entries(plugin.routes ?? {})) {
const refId = `${plugin.getId()}.${name}`;
if (routesById.has(refId)) {
throw new Error(`Unexpected duplicate route '${refId}'`);
}
routesById.set(refId, ref);
}
for (const [name, ref] of Object.entries(plugin.externalRoutes ?? {})) {
const refId = `${plugin.getId()}.${name}`;
if (externalRoutesById.has(refId)) {
throw new Error(`Unexpected duplicate external route '${refId}'`);
}
externalRoutesById.set(refId, ref);
}
}
return { routes: routesById, externalRoutes: externalRoutesById };
}
/** @internal */
export function resolveRouteBindings(
bindRoutes: AppOptions['bindRoutes'],
config: Config,
plugins: Iterable<
Pick<
BackstagePlugin<AnyRoutes, AnyExternalRoutes>,
'getId' | 'routes' | 'externalRoutes'
>
>,
) {
const routesById = collectRouteIds(plugins);
const result = new Map<ExternalRouteRef, RouteRef | SubRouteRef>();
// Perform callback bindings first with highest priority
if (bindRoutes) {
const bind: AppRouteBinder = (
externalRoutes,
@@ -47,5 +98,53 @@ export function resolveRouteBindings(bindRoutes: AppOptions['bindRoutes']) {
bindRoutes({ bind });
}
// Then perform config based bindings with lower priority
const bindings = config
.getOptionalConfig('app.routes.bindings')
?.get<JsonObject>();
if (bindings) {
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`,
);
}
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);
}
}
// Finally fall back to attempting to map defaults, at lowest priority
for (const externalRef of routesById.externalRoutes.values()) {
if (!result.has(externalRef)) {
const defaultRefId =
'getDefaultTarget' in externalRef
? (externalRef.getDefaultTarget as () => string | undefined)()
: undefined;
if (defaultRefId) {
const defaultRef = routesById.routes.get(defaultRefId);
if (defaultRef) {
result.set(externalRef, defaultRef);
}
}
}
}
return result;
}
@@ -66,7 +66,12 @@ export function validateRouteParameters(
// Validates that all non-optional external routes have been bound
export function validateRouteBindings(
routeBindings: Map<ExternalRouteRef, RouteRef | SubRouteRef>,
plugins: Iterable<BackstagePlugin<{}, Record<string, ExternalRouteRef>>>,
plugins: Iterable<
Pick<
BackstagePlugin<{}, Record<string, ExternalRouteRef>>,
'getId' | 'externalRoutes'
>
>,
) {
for (const plugin of plugins) {
if (!plugin.externalRoutes) {
@@ -186,6 +186,10 @@ export function convertLegacyRouteRef(
createExternalRouteRef<{ [key in string]: string }>({
params: legacyRef.params as string[],
optional: legacyRef.optional,
defaultTarget:
'getDefaultTarget' in legacyRef
? (legacyRef.getDefaultTarget as () => string | undefined)()
: undefined,
}),
);
return Object.assign(legacyRef, {
@@ -199,9 +203,9 @@ export function convertLegacyRouteRef(
getDescription() {
return legacyRefStr;
},
// This might already be implemented in the legacy ref, but we override it just to be sure
getDefaultTarget() {
// TODO(freben): These are not yet supported in the old system; just returning undefined for now
return undefined;
return newRef.getDefaultTarget();
},
setId(id: string) {
newRef.setId(id);
+1
View File
@@ -314,6 +314,7 @@ export function createExternalRouteRef<
id: string;
params?: ParamKey[];
optional?: Optional;
defaultTarget?: string;
}): ExternalRouteRef<OptionalParams<Params>, Optional>;
// @public
@@ -38,11 +38,16 @@ export class ExternalRouteRefImpl<
private readonly id: string,
readonly params: ParamKeys<Params>,
readonly optional: Optional,
readonly defaultTarget: string | undefined,
) {}
toString() {
return `routeRef{type=external,id=${this.id}}`;
}
getDefaultTarget() {
return this.defaultTarget;
}
}
/**
@@ -77,10 +82,19 @@ export function createExternalRouteRef<
* if they aren't, `useRouteRef` will return `undefined`.
*/
optional?: Optional;
/**
* The route (typically in another plugin) that this should map to by default.
*
* The string is expected to be on the standard `<plugin id>.<route id>` form,
* for example `techdocs.docRoot`.
*/
defaultTarget?: string;
}): ExternalRouteRef<OptionalParams<Params>, Optional> {
return new ExternalRouteRefImpl(
options.id,
(options.params ?? []) as ParamKeys<OptionalParams<Params>>,
Boolean(options.optional) as Optional,
options?.defaultTarget,
);
}
+1
View File
@@ -26,4 +26,5 @@ export const rootRoute = createRouteRef({
export const registerComponentRouteRef = createExternalRouteRef({
id: 'register-component',
optional: true,
defaultTarget: 'catalog-import.importPage',
});
+1
View File
@@ -38,4 +38,5 @@ export const catalogEntityRouteRef = createExternalRouteRef({
id: 'catalog-entity',
params: ['namespace', 'kind', 'name'],
optional: true,
defaultTarget: 'catalog.catalogEntity',
});
+3
View File
@@ -22,18 +22,21 @@ import {
export const createComponentRouteRef = createExternalRouteRef({
id: 'create-component',
optional: true,
defaultTarget: 'scaffolder.createComponent',
});
export const viewTechDocRouteRef = createExternalRouteRef({
id: 'view-techdoc',
optional: true,
params: ['namespace', 'kind', 'name'],
defaultTarget: 'techdocs.docRoot',
});
export const createFromTemplateRouteRef = createExternalRouteRef({
id: 'create-from-template',
optional: true,
params: ['namespace', 'templateName'],
defaultTarget: 'scaffolder.selectedTemplate',
});
export const unregisterRedirectRouteRef = createExternalRouteRef({
+1 -1
View File
@@ -10,7 +10,7 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
const _default: BackstagePlugin<
{},
{
catalogIndex: ExternalRouteRef<undefined, false>;
catalogIndex: ExternalRouteRef<undefined, true>;
}
>;
export default _default;
+1 -1
View File
@@ -70,7 +70,7 @@ export const MyGroupsSidebarItem: (props: {
const orgPlugin: BackstagePlugin<
{},
{
catalogIndex: ExternalRouteRef<undefined, false>;
catalogIndex: ExternalRouteRef<undefined, true>;
}
>;
export { orgPlugin };
@@ -69,38 +69,45 @@ const EntityCountTile = ({
counter: number;
type?: string;
kind: string;
url: string;
url?: string;
}) => {
const classes = useStyles({ type: type ?? kind });
const rawTitle = type ?? kind;
const isLongText = rawTitle.length > 10;
return (
<Link to={url} variant="body2">
<Box
className={`${classes.card} ${classes.entityTypeBox}`}
display="flex"
flexDirection="column"
alignItems="center"
>
<Typography className={classes.bold} variant="h6">
{counter}
const tile = (
<Box
className={`${classes.card} ${classes.entityTypeBox}`}
display="flex"
flexDirection="column"
alignItems="center"
>
<Typography className={classes.bold} variant="h6">
{counter}
</Typography>
<Box sx={{ width: '100%', textAlign: 'center' }}>
<Typography
className={`${classes.bold} ${isLongText && classes.smallFont}`}
variant="h6"
>
<OverflowTooltip
text={pluralize(rawTitle.toLocaleUpperCase('en-US'), counter)}
/>
</Typography>
<Box sx={{ width: '100%', textAlign: 'center' }}>
<Typography
className={`${classes.bold} ${isLongText && classes.smallFont}`}
variant="h6"
>
<OverflowTooltip
text={pluralize(rawTitle.toLocaleUpperCase('en-US'), counter)}
/>
</Typography>
</Box>
{type && <Typography variant="subtitle1">{kind}</Typography>}
</Box>
</Link>
{type && <Typography variant="subtitle1">{kind}</Typography>}
</Box>
);
if (url) {
return (
<Link to={url} variant="body2">
{tile}
</Link>
);
}
return tile;
};
export const ComponentsGrid = ({
@@ -138,7 +145,7 @@ export const ComponentsGrid = ({
counter={c.counter}
kind={c.kind}
type={c.type}
url={`${catalogLink()}/?${c.queryParams}`}
url={catalogLink && `${catalogLink()}/?${c.queryParams}`}
/>
</Grid>
))}
+2
View File
@@ -18,4 +18,6 @@ import { createExternalRouteRef } from '@backstage/core-plugin-api';
export const catalogIndexRouteRef = createExternalRouteRef({
id: 'catalog-index',
optional: true,
defaultTarget: 'catalog.catalogIndex',
});
+2
View File
@@ -22,12 +22,14 @@ import {
export const registerComponentRouteRef = createExternalRouteRef({
id: 'register-component',
optional: true,
defaultTarget: 'catalog-import.importPage',
});
export const viewTechDocRouteRef = createExternalRouteRef({
id: 'view-techdoc',
optional: true,
params: ['namespace', 'kind', 'name'],
defaultTarget: 'techdocs.docRoot',
});
/**