From 0bf2112f9da9313c0b2afa6aada842950249f309 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 16 Sep 2021 21:58:32 +0200 Subject: [PATCH] Add plugin metadata to route object to simplify navigation tracking. Signed-off-by: Eric Peterson --- packages/core-app-api/src/app/App.tsx | 6 +- .../core-app-api/src/routing/RouteTracker.tsx | 91 ++++++++----------- .../src/routing/collectors.test.tsx | 5 +- .../core-app-api/src/routing/collectors.tsx | 10 +- packages/core-app-api/src/routing/types.ts | 2 + 5 files changed, 58 insertions(+), 56 deletions(-) diff --git a/packages/core-app-api/src/app/App.tsx b/packages/core-app-api/src/app/App.tsx index fb40a83dab..8cb3f167fb 100644 --- a/packages/core-app-api/src/app/App.tsx +++ b/packages/core-app-api/src/app/App.tsx @@ -23,7 +23,7 @@ import React, { useMemo, useState, } from 'react'; -import { createRoutesFromChildren, Route, Routes } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom'; import { useAsync } from 'react-use'; import { ApiProvider, @@ -368,7 +368,7 @@ export class PrivateAppImpl implements BackstageApp { return ( - + {children}} /> @@ -378,7 +378,7 @@ export class PrivateAppImpl implements BackstageApp { return ( - + {children}} /> diff --git a/packages/core-app-api/src/routing/RouteTracker.tsx b/packages/core-app-api/src/routing/RouteTracker.tsx index 673772a605..f53a6fa1e8 100644 --- a/packages/core-app-api/src/routing/RouteTracker.tsx +++ b/packages/core-app-api/src/routing/RouteTracker.tsx @@ -14,70 +14,51 @@ * limitations under the License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; +import { matchRoutes, useLocation } from 'react-router-dom'; import { - createRoutesFromChildren, - matchRoutes, - useLocation, -} from 'react-router-dom'; -import { - BackstagePlugin, useAnalytics, - getComponentData, AnalyticsContext, CommonAnalyticsContext, + RouteRef, } from '@backstage/core-plugin-api'; - -type RouteObjects = ReturnType; +import { routeObjectCollector } from './collectors'; +import { + childDiscoverer, + routeElementDiscoverer, + traverseElementTree, +} from '../extensions/traversal'; +import { BackstageRouteObject } from './types'; /** - * Returns an extension context given the current pathname and a RouteObject - * that defines all registered routes in react. - * - * If no exact match is found, path parts are stripped away, one-by-one, until - * a parent-level path matches a route. + * Returns an extension context given the current pathname and a list of + * Backstage route objects. */ const getExtensionContext = ( pathname: string, - routes: RouteObjects, + routes: BackstageRouteObject[], ): CommonAnalyticsContext | {} => { try { - const cleanPath = pathname.replace(/\/+$/, ''); const matches = matchRoutes(routes, { pathname }); - const RouteElement = matches - ?.filter(match => { - const pathsMatch = match.pathname.replace(/\/+$/, '') === cleanPath; - const hasRoutableElement = !!(match.route.element as React.ReactElement) - ?.props?.element; - return pathsMatch && hasRoutableElement; - }) - .pop()?.route?.element; - const RoutableElement = (RouteElement as React.ReactElement)?.props - ?.element; + const routeObject = matches + ?.filter( + match => (match?.route as BackstageRouteObject).routeRefs?.size > 0, + ) + .pop()?.route as BackstageRouteObject; - if (RoutableElement) { - const plugin: BackstagePlugin | undefined = getComponentData( - RoutableElement, - 'core.plugin', - ); - const mountPoint: { id?: string } | undefined = getComponentData( - RoutableElement, - 'core.mountPoint', - ); - if (plugin && mountPoint) { - return { - pluginId: plugin.getId(), - extension: 'App', - routeRef: mountPoint?.id || '', - }; - } + if (!routeObject) { + return {}; } - // Try again, one path-level shallower. - const nextLevelPath = cleanPath.split('/').slice(0, -1).join('/'); - return nextLevelPath !== '' - ? getExtensionContext(nextLevelPath, routes) - : {}; + const routeRef = routeObject.routeRefs.values().next().value as + | RouteRef + | undefined; + + return { + extension: 'App', + pluginId: routeObject.plugin?.getId() || 'root', + routeRef: (routeRef as { id?: string }).id, + }; } catch { return {}; } @@ -108,12 +89,20 @@ const TrackNavigation = ({ * Logs a "navigate" event with appropriate plugin-level analytics context * attributes each time the user navigates to a page. */ -export const RouteTracker = ({ objects }: { objects: RouteObjects }) => { +export const RouteTracker = ({ tree }: { tree: React.ReactNode }) => { const { pathname, search, hash } = useLocation(); - const attributes = getExtensionContext(pathname, objects); + const { routeObjects } = useMemo(() => { + return traverseElementTree({ + root: tree, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routeObjects: routeObjectCollector, + }, + }); + }, [tree]); return ( - + ); diff --git a/packages/core-app-api/src/routing/collectors.test.tsx b/packages/core-app-api/src/routing/collectors.test.tsx index 36d99d2938..5e8e22051d 100644 --- a/packages/core-app-api/src/routing/collectors.test.tsx +++ b/packages/core-app-api/src/routing/collectors.test.tsx @@ -32,6 +32,7 @@ import { createPlugin, RouteRef, attachComponentData, + BackstagePlugin, } from '@backstage/core-plugin-api'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; @@ -98,6 +99,7 @@ function routeObj( refs: RouteRef[], children: any[] = [], type: 'mounted' | 'gathered' = 'mounted', + backstagePlugin?: BackstagePlugin, ) { return { path: path, @@ -113,6 +115,7 @@ function routeObj( }, ...children, ], + plugin: backstagePlugin, }; } @@ -186,7 +189,7 @@ describe('discovery', () => { routeObj('/blop', [ref5]), ], ), - routeObj('/divsoup', [ref4]), + routeObj('/divsoup', [ref4], undefined, undefined, plugin), ]); }); diff --git a/packages/core-app-api/src/routing/collectors.tsx b/packages/core-app-api/src/routing/collectors.tsx index 0ba83e90f8..2d044b4b23 100644 --- a/packages/core-app-api/src/routing/collectors.tsx +++ b/packages/core-app-api/src/routing/collectors.tsx @@ -15,7 +15,11 @@ */ import { isValidElement, ReactElement, ReactNode } from 'react'; -import { RouteRef, getComponentData } from '@backstage/core-plugin-api'; +import { + RouteRef, + getComponentData, + BackstagePlugin, +} from '@backstage/core-plugin-api'; import { BackstageRouteObject } from './types'; import { createCollector } from '../extensions/traversal'; import { FeatureFlagged, FeatureFlaggedProps } from './FeatureFlagged'; @@ -141,6 +145,10 @@ export const routeObjectCollector = createCollector( element: 'mounted', routeRefs: new Set([routeRef]), children: [MATCH_ALL_ROUTE], + plugin: getComponentData( + node.props.element, + 'core.plugin', + ), }; parentChildren.push(newObject); return newObject; diff --git a/packages/core-app-api/src/routing/types.ts b/packages/core-app-api/src/routing/types.ts index 12bf0d1a0f..e2cf665982 100644 --- a/packages/core-app-api/src/routing/types.ts +++ b/packages/core-app-api/src/routing/types.ts @@ -18,6 +18,7 @@ import { RouteRef, SubRouteRef, ExternalRouteRef, + BackstagePlugin, } from '@backstage/core-plugin-api'; import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; @@ -53,6 +54,7 @@ export interface BackstageRouteObject { element: React.ReactNode; path: string; routeRefs: Set; + plugin?: BackstagePlugin; } export function isRouteRef(