declaratively disable external routes

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-06-24 15:46:20 +02:00
parent 0c5aa5a007
commit d3c39fc71c
5 changed files with 133 additions and 26 deletions
+15
View File
@@ -0,0 +1,15 @@
---
'@backstage/core-app-api': minor
'@backstage/frontend-app-api': patch
---
Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets.
```yaml
app:
routes:
bindings:
# This has the effect of removing the button for registering new
# catalog entities in the scaffolder template list view
scaffolder.registerComponent: false
```
@@ -194,4 +194,35 @@ describe('collectRouteIds', () => {
'test.extRef': extRef,
});
});
it('can disable external routes that have defaults', () => {
const source = createExternalRouteRef({
id: 'test',
defaultTarget: 'test.target1',
});
const target1 = createRouteRef({ id: 'test' });
const plugin = createPlugin({
id: 'test',
routes: { target1 },
externalRoutes: { source },
});
// resolves normally with no config
let result = resolveRouteBindings(() => {}, new MockConfigApi({}), [
plugin,
]);
expect(result.get(source)).toBe(target1);
// can be disabled
result = resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.source': false } } },
}),
[plugin],
);
expect(result.get(source)).toBe(undefined);
});
});
@@ -102,11 +102,12 @@ 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 (typeof targetRefId !== 'string' || targetRefId === '') {
if (!isValidTargetRefId(targetRefId)) {
throw new Error(
`Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string`,
`Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string or false`,
);
}
@@ -116,23 +117,27 @@ export function resolveRouteBindings(
`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);
if (targetRefId === false) {
disabledExternalRefs.add(externalRef);
result.delete(externalRef);
} else if (!result.has(externalRef)) {
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)) {
if (!result.has(externalRef) && !disabledExternalRefs.has(externalRef)) {
const defaultRefId =
'getDefaultTarget' in externalRef
? (externalRef.getDefaultTarget as () => string | undefined)()
@@ -148,3 +153,15 @@ export function resolveRouteBindings(
return result;
}
function isValidTargetRefId(value: unknown): value is string | false {
if (value === false) {
return true;
}
if (typeof value === 'string' && value) {
return true;
}
return false;
}
@@ -161,4 +161,31 @@ describe('resolveRouteBindings', () => {
expect(result.get(source)).toBe(target2);
});
it('can disable external routes that have defaults', () => {
const source = createExternalRouteRef({ defaultTarget: 'target1' });
const target1 = createRouteRef();
const routesById = {
routes: new Map([['target1', target1]]),
externalRoutes: new Map([['source', source]]),
};
// resolves normally with no config
let result = resolveRouteBindings(
() => {},
new ConfigReader({}),
routesById,
);
expect(result.get(source)).toBe(target1);
// can be disabled
result = resolveRouteBindings(
() => {},
new ConfigReader({ app: { routes: { bindings: { source: false } } } }),
routesById,
);
expect(result.get(source)).toBe(undefined);
});
});
@@ -112,11 +112,12 @@ 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 (typeof targetRefId !== 'string' || targetRefId === '') {
if (!isValidTargetRefId(targetRefId)) {
throw new Error(
`Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string`,
`Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string or false`,
);
}
@@ -126,23 +127,27 @@ export function resolveRouteBindings(
`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);
if (targetRefId === false) {
disabledExternalRefs.add(externalRef);
result.delete(externalRef);
} else if (!result.has(externalRef)) {
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)) {
if (!result.has(externalRef) && !disabledExternalRefs.has(externalRef)) {
const defaultRefId =
toInternalExternalRouteRef(externalRef).getDefaultTarget();
if (defaultRefId) {
@@ -156,3 +161,15 @@ export function resolveRouteBindings(
return result;
}
function isValidTargetRefId(value: unknown): value is string | false {
if (value === false) {
return true;
}
if (typeof value === 'string' && value) {
return true;
}
return false;
}