{core,frontend}-app-api: fix route binding prio + disable through code

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-09-12 12:32:21 +02:00
parent fec4cd9876
commit ddbeace2a4
8 changed files with 132 additions and 17 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-app-api': minor
'@backstage/frontend-app-api': patch
---
Added the ability to explicitly disable routes through the `bindRoutes` option by passing `false` as the route target. This also fixes a bug where route bindings in config were incorrectly prioritized above the ones in code in certain situations.
+16 -6
View File
@@ -91,19 +91,29 @@ There could be situations where you would like to disable the
![Disable Button](../../assets/software-templates/disable-register-existing-component-button.png)
To do so, you will un-register / remove the `catalogImportPlugin.routes.importPage`
from `backstage/packages/app/src/App.tsx`:
To do so, you need to explicitly disable the default route binding from the `scaffolderPlugin.registerComponent` to the Catalog Import page.
This can be done in `backstage/packages/app/src/App.tsx`:
```diff
const app = createApp({
apis,
bindRoutes({ bind }) {
- bind(scaffolderPlugin.externalRoutes, {
bind(scaffolderPlugin.externalRoutes, {
+ registerComponent: false,
- registerComponent: catalogImportPlugin.routes.importPage,
- });
bind(orgPlugin.externalRoutes, {
catalogIndex: catalogPlugin.routes.catalogIndex,
viewTechDoc: techdocsPlugin.routes.docRoot,
});
})
```
OR in `app-config.yaml`:
```yaml
app:
routes:
bindings:
scaffolder.registerComponent: false
```
After the change, you should no longer see the button.
@@ -251,6 +251,8 @@ app:
# point to the Catalog details page when the Scaffolder component details ref is used
# highlight-next-line
scaffolder.componentDetails: catalog.details
# explicitly disable the default route binding from the scaffolder to the catalog import page
scaffolder.registerComponent: false
```
We also have the ability to express this in code as an option to `createApp`, but you of course only need to use one of these two methods:
@@ -268,6 +270,7 @@ const app = createApp({
});
bind(scaffolder.externalRoutes, {
componentDetails: catalog.routes.details,
registerComponent: false,
});
},
// highlight-end
@@ -75,6 +75,55 @@ describe('resolveRouteBindings', () => {
expect(result.get(mySource)).toBe(myTarget);
});
it('prioritizes callback routes over config', () => {
const mySource = createExternalRouteRef({ id: 'test', optional: true });
const myTarget = createRouteRef({ id: 'test' });
expect(
resolveRouteBindings(
({ bind }) => {
bind({ mySource }, { mySource: false });
},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': 'myTarget' } } },
}),
[
createPlugin({
id: 'test',
routes: {
myTarget,
},
externalRoutes: {
mySource,
},
}),
],
).get(mySource),
).toBe(undefined);
expect(
resolveRouteBindings(
({ bind }) => {
bind({ mySource }, { mySource: myTarget });
},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': false } } },
}),
[
createPlugin({
id: 'test',
routes: {
myTarget,
},
externalRoutes: {
mySource,
},
}),
],
).get(mySource),
).toBe(myTarget);
});
it('throws on invalid config', () => {
expect(() =>
resolveRouteBindings(
@@ -73,6 +73,7 @@ export function resolveRouteBindings(
) {
const routesById = collectRouteIds(plugins);
const result = new Map<ExternalRouteRef, RouteRef | SubRouteRef>();
const disabledExternalRefs = new Set<ExternalRouteRef>();
// Perform callback bindings first with highest priority
if (bindRoutes) {
@@ -87,11 +88,15 @@ export function resolveRouteBindings(
}
if (!value && !externalRoute.optional) {
throw new Error(
`External route ${key} is required but was undefined`,
`External route ${key} is required but was ${
value === false ? 'disabled' : 'not provided'
}`,
);
}
if (value) {
result.set(externalRoute, value);
} else if (value === false) {
disabledExternalRefs.add(externalRoute);
}
}
};
@@ -102,7 +107,6 @@ export function resolveRouteBindings(
const bindings = config
.getOptionalConfig('app.routes.bindings')
?.get<JsonObject>();
const disabledExternalRefs = new Set<ExternalRouteRef>();
if (bindings) {
for (const [externalRefId, targetRefId] of Object.entries(bindings)) {
if (!isValidTargetRefId(targetRefId)) {
@@ -118,11 +122,14 @@ export function resolveRouteBindings(
);
}
// Skip if binding was already defined in code
if (result.has(externalRef) || disabledExternalRefs.has(externalRef)) {
continue;
}
if (targetRefId === false) {
disabledExternalRefs.add(externalRef);
result.delete(externalRef);
} else if (!result.has(externalRef)) {
} else {
const targetRef = routesById.routes.get(targetRefId);
if (!targetRef) {
throw new Error(
+1 -1
View File
@@ -160,7 +160,7 @@ type TargetRouteMap<
infer Params,
any
>
? RouteRef<Params> | SubRouteRef<Params>
? RouteRef<Params> | SubRouteRef<Params> | false
: never;
};
@@ -69,6 +69,41 @@ describe('resolveRouteBindings', () => {
expect(result.get(mySource)).toBe(myTarget);
});
it('prioritizes callback routes over config', () => {
const mySource = createExternalRouteRef();
const myTarget = createRouteRef();
expect(
resolveRouteBindings(
({ bind }) => {
bind({ mySource }, { mySource: false });
},
new ConfigReader({
app: { routes: { bindings: { mySource: 'myTarget' } } },
}),
{
routes: new Map([['myTarget', myTarget]]),
externalRoutes: new Map([['mySource', mySource]]),
},
).get(mySource),
).toBe(undefined);
expect(
resolveRouteBindings(
({ bind }) => {
bind({ mySource }, { mySource: myTarget });
},
new ConfigReader({
app: { routes: { bindings: { mySource: false } } },
}),
{
routes: new Map([['myTarget', myTarget]]),
externalRoutes: new Map([['mySource', mySource]]),
},
).get(mySource),
).toBe(myTarget);
});
it('throws on invalid config', () => {
expect(() =>
resolveRouteBindings(
@@ -55,7 +55,7 @@ type TargetRouteMap<
[name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef<
infer Params
>
? RouteRef<Params> | SubRouteRef<Params>
? RouteRef<Params> | SubRouteRef<Params> | false
: never;
};
@@ -82,6 +82,7 @@ export function resolveRouteBindings(
routesById: RouteRefsById,
): Map<ExternalRouteRef, RouteRef | SubRouteRef> {
const result = new Map<ExternalRouteRef, RouteRef | SubRouteRef>();
const disabledExternalRefs = new Set<ExternalRouteRef>();
// Perform callback bindings first with highest priority
if (bindRoutes) {
@@ -96,6 +97,8 @@ export function resolveRouteBindings(
}
if (value) {
result.set(externalRoute, value);
} else if (value === false) {
disabledExternalRefs.add(externalRoute);
}
}
};
@@ -106,7 +109,6 @@ export function resolveRouteBindings(
const bindings = config
.getOptionalConfig('app.routes.bindings')
?.get<JsonObject>();
const disabledExternalRefs = new Set<ExternalRouteRef>();
if (bindings) {
for (const [externalRefId, targetRefId] of Object.entries(bindings)) {
if (!isValidTargetRefId(targetRefId)) {
@@ -122,11 +124,14 @@ export function resolveRouteBindings(
);
}
// Skip if binding was already defined in code
if (result.has(externalRef) || disabledExternalRefs.has(externalRef)) {
continue;
}
if (targetRefId === false) {
disabledExternalRefs.add(externalRef);
result.delete(externalRef);
} else if (!result.has(externalRef)) {
} else {
const targetRef = routesById.routes.get(targetRefId);
if (!targetRef) {
throw new Error(