frontend-plugin-api: initial support for subpages

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-02-09 14:09:00 +01:00
parent 9e71e1dec8
commit 4b996d05d3
22 changed files with 1570 additions and 31 deletions
@@ -15,8 +15,6 @@
*/
import { Content, Header, HeaderTabs, Page } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { appTreeApiRef } from '@backstage/frontend-plugin-api';
import { Flex } from '@backstage/ui';
import { useCallback, useEffect, useMemo } from 'react';
import { DetailedVisualizer } from './DetailedVisualizer';
@@ -31,31 +29,28 @@ import {
} from 'react-router-dom';
export function AppVisualizerPage() {
const appTreeApi = useApi(appTreeApiRef);
const { tree } = appTreeApi.getTree();
const tabs = useMemo(
() => [
{
id: 'tree',
path: 'tree',
label: 'Tree',
element: <TreeVisualizer tree={tree} />,
element: <TreeVisualizer />,
},
{
id: 'detailed',
path: 'detailed',
label: 'Detailed',
element: <DetailedVisualizer tree={tree} />,
element: <DetailedVisualizer />,
},
{
id: 'text',
path: 'text',
label: 'Text',
element: <TextVisualizer tree={tree} />,
element: <TextVisualizer />,
},
],
[tree],
[],
);
const location = useLocation();
@@ -16,13 +16,13 @@
import {
AppNode,
AppTree,
ExtensionDataRef,
coreExtensionData,
ApiBlueprint,
NavItemBlueprint,
useApi,
routeResolutionApiRef,
appTreeApiRef,
} from '@backstage/frontend-plugin-api';
import { Box, Flex, Link, Text, Tooltip, TooltipTrigger } from '@backstage/ui';
import {
@@ -351,7 +351,10 @@ function Legend() {
);
}
export function DetailedVisualizer({ tree }: { tree: AppTree }) {
export function DetailedVisualizer() {
const appTreeApi = useApi(appTreeApiRef);
const { tree } = appTreeApi.getTree();
return (
<Flex direction="column" style={{ height: '100%', flex: '1 1 100%' }}>
<Box ml="4" mt="4" style={{ flex: '1 1 0', overflow: 'auto' }}>
@@ -14,7 +14,11 @@
* limitations under the License.
*/
import { AppNode, AppTree } from '@backstage/frontend-plugin-api';
import {
AppNode,
useApi,
appTreeApiRef,
} from '@backstage/frontend-plugin-api';
import { Box, Checkbox } from '@backstage/ui';
import { ReactNode, useState } from 'react';
@@ -77,7 +81,9 @@ function nodeToText(
]);
}
export function TextVisualizer({ tree }: { tree: AppTree }) {
export function TextVisualizer() {
const appTreeApi = useApi(appTreeApiRef);
const { tree } = appTreeApi.getTree();
const [showOutputs, setShowOutputs] = useState(false);
const [showDisabled, setShowDisabled] = useState(false);
@@ -18,7 +18,12 @@ import {
DependencyGraph,
DependencyGraphTypes,
} from '@backstage/core-components';
import { AppNode, AppTree } from '@backstage/frontend-plugin-api';
import {
AppNode,
AppTree,
useApi,
appTreeApiRef,
} from '@backstage/frontend-plugin-api';
import { Flex } from '@backstage/ui';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
@@ -137,7 +142,9 @@ export function Node(props: { node: NodeType }) {
);
}
export function TreeVisualizer({ tree }: { tree: AppTree }) {
export function TreeVisualizer() {
const appTreeApi = useApi(appTreeApiRef);
const { tree } = appTreeApi.getTree();
const graphData = useMemo(() => resolveGraphData(tree), [tree]);
return (
+89 -4
View File
@@ -19,6 +19,7 @@ import {
createRouteRef,
NavItemBlueprint,
PageBlueprint,
SubPageBlueprint,
} from '@backstage/frontend-plugin-api';
import { RiEyeLine as VisualizerIcon } from '@remixicon/react';
@@ -28,10 +29,87 @@ const appVisualizerPage = PageBlueprint.make({
params: {
path: '/visualizer',
routeRef: rootRouteRef,
title: 'Visualizer',
// loader: async () => <div>The root page</div>,
},
});
const appVisualizerPage2 = PageBlueprint.make({
name: '2',
params: {
path: '/visualizer/something-else',
routeRef: createRouteRef(),
title: 'something else',
loader: async () => <div>The root sdsd</div>,
},
});
/*
// inputs:
// pages: [explicitly subrouteref as a data type + element + path]
const rootPage = TabbedPageBlueprint.make({
params: {
path: '/visualizer',
routeRef: rootRouteRef,
actions: [
]
subpages: [
]
}
})
const treePageThing = PluginContentTopBarNavigableContentBlueprint.make({
attachTo: {
}
params: {
routeRef: treeSubRouteRef,
loader: () => null,
}
})
*/
const treeRouteRef = createRouteRef();
const detailedRouteRef = createRouteRef();
const textRouteRef = createRouteRef();
const appVisualizerTreePage = SubPageBlueprint.make({
attachTo: { id: 'page:app-visualizer', input: 'pages' },
name: 'tree',
params: {
path: 'tree',
routeRef: treeRouteRef,
title: 'Tree',
loader: () =>
import('./components/AppVisualizerPage').then(m => (
<m.AppVisualizerPage />
)),
import('./components/AppVisualizerPage/TreeVisualizer').then(
m => <m.TreeVisualizer />,
),
},
});
const appVisualizerDetailedPage = SubPageBlueprint.make({
attachTo: { id: 'page:app-visualizer', input: 'pages' },
name: 'details',
params: {
path: 'details',
routeRef: detailedRouteRef,
title: 'Detailed',
loader: () =>
import('./components/AppVisualizerPage/DetailedVisualizer').then(
m => <m.DetailedVisualizer />,
),
},
});
const appVisualizerTextPage = SubPageBlueprint.make({
attachTo: { id: 'page:app-visualizer', input: 'pages' },
name: 'text',
params: {
path: 'text',
routeRef: textRouteRef,
title: 'Text',
loader: () =>
import('./components/AppVisualizerPage/TextVisualizer').then(
m => <m.TextVisualizer />,
),
},
});
@@ -47,5 +125,12 @@ export const appVisualizerNavItem = NavItemBlueprint.make({
export const visualizerPlugin = createFrontendPlugin({
pluginId: 'app-visualizer',
info: { packageJson: () => import('../package.json') },
extensions: [appVisualizerPage, appVisualizerNavItem],
extensions: [
appVisualizerPage,
appVisualizerPage2,
appVisualizerTreePage,
appVisualizerDetailedPage,
appVisualizerTextPage,
appVisualizerNavItem,
],
});