diff --git a/.changeset/thick-poems-camp.md b/.changeset/thick-poems-camp.md
new file mode 100644
index 0000000000..72056dfb5a
--- /dev/null
+++ b/.changeset/thick-poems-camp.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-app-api': patch
+---
+
+Fixed a bug where `useRouteRef` would fail in situations where relative navigation was needed and the app was is mounted on a sub-path. This would typically show up as a failure to navigate to a tab on an entity page.
diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md
index 5a970ef143..02e2e4d51d 100644
--- a/docs/getting-started/index.md
+++ b/docs/getting-started/index.md
@@ -23,7 +23,7 @@ guide to do a repository-based installation.
- Access to a Linux-based operating system, such as Linux, MacOS or
[Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/)
-- An account with elevated rights
+- An account with elevated rights to install the dependencies
- `curl` or `wget` installed
- Node.js Active LTS Release installed (currently v14) using one of these
methods:
@@ -36,15 +36,16 @@ guide to do a repository-based installation.
- `yarn` [Installation](https://classic.yarnpkg.com/en/docs/install)
- `docker` [installation](https://docs.docker.com/engine/install/)
- `git` [installation](https://github.com/git-guides/install-git)
-- If the system is not directly accessible over your network, the following
- ports need to be opened: 3000, 7007
+- If the system is not directly accessible over your network the following ports
+ need to be opened: 3000, 7007. This is quite uncommon, unless when you're
+ installing in a container, VM or remote system.
### Create your Backstage App
To install the Backstage Standalone app, we make use of `npx`, a tool to run
-Node executables straight from the registry. Running the command below will
-install Backstage. The wizard will create a subdirectory inside your current
-working directory.
+Node executables straight from the registry. This tool is part of your Node.js
+installation. Running the command below will install Backstage. The wizard will
+create a subdirectory inside your current working directory.
```bash
npx @backstage/create-app
@@ -78,12 +79,21 @@ yarn dev
It might take a little while, but as soon as the message
`[0] webpack compiled successfully` appears, you can open a browser and directly
navigate to your freshly installed Backstage portal at `http://localhost:3000`.
-You can start exploring the demo immediately.
+You can start exploring the demo immediately. Please note that the in-memory
+database will be cleared when you restart the app, so you'll most likely want to
+carry on with the database steps.
+The most common next steps are to move to a persistent database, configure
+authentication, and add a plugin:
+
+- [Switching from SQLite to PostgresQL](https://backstage.io/docs/tutorials/switching-sqlite-postgres)
+- [Setting up Authentication](https://backstage.io/docs/auth/)
+- [Adding a plugin](https://backstage.io/docs/getting-started/configure-app-with-plugins)
+
Congratulations! That should be it. Let us know how it went:
[on discord](https://discord.gg/EBHEGzX), file issues for any
[feature](https://github.com/backstage/backstage/issues/new?labels=help+wanted&template=feature_template.md)
@@ -93,10 +103,3 @@ or
[bugs](https://github.com/backstage/backstage/issues/new?labels=bug&template=bug_template.md)
you have, and feel free to
[contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)!
-
-The most common next steps are to configure Backstage, add a plugin and moving
-to a more persistent database:
-
-- [Setting up Authentication](https://backstage.io/docs/auth/)
-- [Switching from SQLite to PostgresQL](https://backstage.io/docs/tutorials/switching-sqlite-postgres)
-- [Adding a plugin](https://backstage.io/docs/getting-started/configure-app-with-plugins)
diff --git a/packages/core-app-api/src/routing/RouteResolver.test.ts b/packages/core-app-api/src/routing/RouteResolver.test.ts
index 1b7eb10003..b068bd5e6d 100644
--- a/packages/core-app-api/src/routing/RouteResolver.test.ts
+++ b/packages/core-app-api/src/routing/RouteResolver.test.ts
@@ -100,23 +100,55 @@ describe('RouteResolver', () => {
expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined);
});
- it('should resolve an absolute route and an app base path', () => {
+ it('should resolve an absolute route and sub route with an app base path', () => {
const r = new RouteResolver(
- new Map([[ref1, '/my-route']]),
- new Map(),
- [{ routeRefs: new Set([ref1]), path: '/my-route', ...rest }],
+ new Map([
+ [ref2, '/my-parent/:x'],
+ [ref1, '/my-route'],
+ ]),
+ new Map([[ref1, ref2]]),
+ [
+ {
+ routeRefs: new Set([ref2]),
+ path: '/my-parent/:x',
+ ...rest,
+ children: [
+ MATCH_ALL_ROUTE,
+ { routeRefs: new Set([ref1]), path: '/my-route', ...rest },
+ ],
+ },
+ ],
new Map(),
'/base',
);
- expect(r.resolve(ref1, '/')?.()).toBe('/base/my-route');
- expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined);
- expect(r.resolve(subRef1, '/')?.()).toBe('/base/my-route/foo');
- expect(r.resolve(subRef2, '/')?.({ a: '2a' })).toBe(
- '/base/my-route/foo/2a',
+ expect(r.resolve(ref1, '/my-parent/1x')?.()).toBe(
+ '/base/my-parent/1x/my-route',
+ );
+ expect(r.resolve(ref1, '/base/my-parent/1x')?.()).toBe(
+ '/base/my-parent/1x/my-route',
+ );
+ expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe('/base/my-parent/1x');
+ expect(r.resolve(ref2, '/base')?.({ x: '1x' })).toBe('/base/my-parent/1x');
+ expect(r.resolve(ref3, '/')?.({ y: '1y' })).toBe(undefined);
+ expect(r.resolve(subRef1, '/my-parent/2x')?.()).toBe(
+ '/base/my-parent/2x/my-route/foo',
+ );
+ expect(r.resolve(subRef1, '/base/my-parent/2x')?.()).toBe(
+ '/base/my-parent/2x/my-route/foo',
+ );
+ expect(r.resolve(subRef2, '/my-parent/3x')?.({ a: '2a' })).toBe(
+ '/base/my-parent/3x/my-route/foo/2a',
+ );
+ expect(r.resolve(subRef2, '/base/my-parent/3x')?.({ a: '2a' })).toBe(
+ '/base/my-parent/3x/my-route/foo/2a',
+ );
+ expect(r.resolve(subRef3, '/')?.({ x: '5x' })).toBe(
+ '/base/my-parent/5x/bar',
+ );
+ expect(r.resolve(subRef4, '/')?.({ x: '6x', a: '4a' })).toBe(
+ '/base/my-parent/6x/bar/4a',
);
- expect(r.resolve(subRef3, '/')?.({ x: '3x' })).toBe(undefined);
- expect(r.resolve(subRef4, '/')?.({ x: '4x', a: '4a' })).toBe(undefined);
expect(r.resolve(externalRef1, '/')?.()).toBe(undefined);
expect(r.resolve(externalRef2, '/')?.()).toBe(undefined);
expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe(undefined);
diff --git a/packages/core-app-api/src/routing/RouteResolver.ts b/packages/core-app-api/src/routing/RouteResolver.ts
index b5be5f893e..f186b28586 100644
--- a/packages/core-app-api/src/routing/RouteResolver.ts
+++ b/packages/core-app-api/src/routing/RouteResolver.ts
@@ -207,6 +207,20 @@ export class RouteResolver {
return undefined;
}
+ // The location that we get passed in uses the full path, so start by trimming off
+ // the app base path prefix in case we're running the app on a sub-path.
+ let relativeSourceLocation: Parameters[1];
+ if (typeof sourceLocation === 'string') {
+ relativeSourceLocation = this.trimPath(sourceLocation);
+ } else if (sourceLocation.pathname) {
+ relativeSourceLocation = {
+ ...sourceLocation,
+ pathname: this.trimPath(sourceLocation.pathname),
+ };
+ } else {
+ relativeSourceLocation = sourceLocation;
+ }
+
// Next we figure out the base path, which is the combination of the common parent path
// between our current location and our target location, as well as the additional path
// that is the difference between the parent path and the base of our target location.
@@ -214,7 +228,7 @@ export class RouteResolver {
this.appBasePath +
resolveBasePath(
targetRef,
- sourceLocation,
+ relativeSourceLocation,
this.routePaths,
this.routeParents,
this.routeObjects,
@@ -225,4 +239,15 @@ export class RouteResolver {
};
return routeFunc;
}
+
+ private trimPath(targetPath: string) {
+ if (!targetPath) {
+ return targetPath;
+ }
+
+ if (targetPath.startsWith(this.appBasePath)) {
+ return targetPath.slice(this.appBasePath.length);
+ }
+ return targetPath;
+ }
}