feat(devtools): add extension to allow providing tabs

Signed-off-by: secustor <sebastian@poxhofer.at>
This commit is contained in:
secustor
2025-09-03 17:07:02 +02:00
parent b9318d4ecf
commit 1f95d7903d
14 changed files with 208 additions and 12 deletions
@@ -0,0 +1,81 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createExtensionBlueprint } from '@backstage/frontend-plugin-api';
import { devToolsRouteDataRef } from './devToolsRouteDataRef';
import { lazy, Suspense, JSX } from 'react';
/**
* Parameters for creating a DevTools route extension
* @alpha
*/
export interface DevToolsRouteBlueprintParams {
path: string;
title: string;
loader: () => Promise<{ default: () => JSX.Element }> | (() => JSX.Element);
}
/**
* Extension blueprint for creating DevTools routes
*
* @example
* ```tsx
* const myDevToolsRoute = DevToolsRouteBlueprint.make({
* params: {
* path: 'my-feature',
* title: 'My Feature',
* loader: () => import('./MyContent').then(m => ({ default: m.MyContent }))
* }
* });
* ```
*
* @alpha
*/
export const DevToolsRouteBlueprint = createExtensionBlueprint({
kind: 'devtools-route',
attachTo: { id: 'page:devtools', input: 'routes' },
output: [devToolsRouteDataRef],
factory(params: DevToolsRouteBlueprintParams) {
// Handle both sync and async component loading
const Component = typeof params.loader === 'function'
? (() => {
const result = params.loader();
if (result instanceof Promise) {
// Create a lazy component for async loading
return lazy(() => result);
}
// Direct component function
return result;
})()
: params.loader;
const children = typeof Component === 'function'
? <Suspense fallback={<div>Loading...</div>}><Component /></Suspense>
: Component;
return [
devToolsRouteDataRef({
path: params.path,
title: params.title,
children,
}),
];
},
dataRefs: {
route: devToolsRouteDataRef,
},
});
@@ -0,0 +1,37 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createExtensionDataRef } from '@backstage/frontend-plugin-api';
import { JSX } from 'react';
/**
* Represents a DevTools route that can be contributed by extensions
* @alpha
*/
export interface DevToolsRouteData {
path: string;
title: string;
children: JSX.Element;
}
/**
* Extension data reference for DevTools routes
* @alpha
*/
export const devToolsRouteDataRef = createExtensionDataRef<DevToolsRouteData>()
.with({
id: 'devtools.route',
});
+4
View File
@@ -15,3 +15,7 @@
*/
export { default } from './plugin';
export { DevToolsRouteBlueprint } from './devToolsRouteBlueprint';
export { devToolsRouteDataRef } from './devToolsRouteDataRef';
export type { DevToolsRouteData } from './devToolsRouteDataRef';
export type { DevToolsRouteBlueprintParams } from './devToolsRouteBlueprint';
+17 -8
View File
@@ -21,6 +21,7 @@ import {
ApiBlueprint,
PageBlueprint,
NavItemBlueprint,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import { devToolsApiRef, DevToolsClient } from '../api';
@@ -30,6 +31,7 @@ import {
} from '@backstage/core-compat-api';
import BuildIcon from '@material-ui/icons/Build';
import { rootRouteRef } from '../routes';
import { devToolsRouteDataRef } from './devToolsRouteDataRef';
/** @alpha */
export const devToolsApi = ApiBlueprint.make({
@@ -46,14 +48,21 @@ export const devToolsApi = ApiBlueprint.make({
});
/** @alpha */
export const devToolsPage = PageBlueprint.make({
params: {
path: '/devtools',
routeRef: convertLegacyRouteRef(rootRouteRef),
loader: () =>
import('../components/DevToolsPage').then(m =>
compatWrapper(<m.DevToolsPage />),
),
export const devToolsPage = PageBlueprint.makeWithOverrides({
inputs: {
routes: createExtensionInput([devToolsRouteDataRef], {
optional: true,
}),
},
factory(originalFactory, { inputs }) {
return originalFactory({
path: '/devtools',
routeRef: convertLegacyRouteRef(rootRouteRef),
loader: () =>
import('../components/DevToolsPage').then(m =>
compatWrapper(<m.DevToolsPage extensionRoutes={inputs.routes?.map(route => route.get(devToolsRouteDataRef))} />),
),
});
},
});
@@ -23,9 +23,14 @@ import { ConfigContent } from '../Content/ConfigContent';
import { DevToolsLayout } from '../DevToolsLayout';
import { InfoContent } from '../Content/InfoContent';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { DevToolsRouteData } from '../../alpha/devToolsRouteDataRef';
export interface DefaultDevToolsPageProps {
extensionRoutes?: DevToolsRouteData[];
}
/** @public */
export const DefaultDevToolsPage = () => (
export const DefaultDevToolsPage = ({ extensionRoutes }: DefaultDevToolsPageProps) => (
<DevToolsLayout>
<DevToolsLayout.Route path="info" title="Info">
<RequirePermission permission={devToolsInfoReadPermission}>
@@ -37,5 +42,14 @@ export const DefaultDevToolsPage = () => (
<ConfigContent />
</RequirePermission>
</DevToolsLayout.Route>
{extensionRoutes?.map((route, index) => (
<DevToolsLayout.Route
key={`extension-${route.path}-${index}`}
path={route.path}
title={route.title}
>
{route.children}
</DevToolsLayout.Route>
))}
</DevToolsLayout>
);
@@ -16,9 +16,14 @@
import { useOutlet } from 'react-router-dom';
import { DefaultDevToolsPage } from '../DefaultDevToolsPage';
import { DevToolsRouteData } from '../../alpha/devToolsRouteDataRef';
export const DevToolsPage = () => {
export interface DevToolsPageProps {
extensionRoutes?: DevToolsRouteData[];
}
export const DevToolsPage = ({ extensionRoutes }: DevToolsPageProps) => {
const outlet = useOutlet();
return <>{outlet || <DefaultDevToolsPage />}</>;
return <>{outlet || <DefaultDevToolsPage extensionRoutes={extensionRoutes} />}</>;
};