From 1f95d7903d4015e39dc3abd97b35166944ab70ac Mon Sep 17 00:00:00 2001 From: secustor Date: Wed, 3 Sep 2025 17:07:02 +0200 Subject: [PATCH] feat(devtools): add extension to allow providing tabs Signed-off-by: secustor --- package.json | 3 + packages/app-next/package.json | 1 + packages/app-next/src/App.tsx | 5 +- packages/backend/package.json | 3 + .../catalog-unprocessed-entities/package.json | 1 + .../src/alpha/devToolsRoute.ts | 32 ++++++++ .../src/alpha/index.ts | 1 + .../src/alpha/devToolsRouteBlueprint.tsx | 81 +++++++++++++++++++ .../src/alpha/devToolsRouteDataRef.ts | 37 +++++++++ plugins/devtools/src/alpha/index.ts | 4 + plugins/devtools/src/alpha/plugin.tsx | 25 ++++-- .../DefaultDevToolsPage.tsx | 16 +++- .../components/DevToolsPage/DevToolsPage.tsx | 9 ++- yarn.lock | 2 + 14 files changed, 208 insertions(+), 12 deletions(-) create mode 100644 plugins/catalog-unprocessed-entities/src/alpha/devToolsRoute.ts create mode 100644 plugins/devtools/src/alpha/devToolsRouteBlueprint.tsx create mode 100644 plugins/devtools/src/alpha/devToolsRouteDataRef.ts diff --git a/package.json b/package.json index d4ebc22bc1..88238a1a21 100644 --- a/package.json +++ b/package.json @@ -185,5 +185,8 @@ } }, "tsConfig": "./tsconfig.json" + }, + "volta": { + "node": "22.19.0" } } diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 44602b2438..0a39471e34 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -56,6 +56,7 @@ "@backstage/plugin-catalog-import": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-catalog-unprocessed-entities": "workspace:^", + "@backstage/plugin-devtools": "workspace:^", "@backstage/plugin-home": "workspace:^", "@backstage/plugin-kubernetes": "workspace:^", "@backstage/plugin-kubernetes-cluster": "workspace:^", diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 6c567f3fe1..92d1f06d6a 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -45,6 +45,8 @@ import { convertLegacyPageExtension } from '@backstage/core-compat-api'; import { convertLegacyEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; import { pluginInfoResolver } from './pluginInfoResolver'; import { appModuleNav } from './modules/appModuleNav'; +import devtoolsPlugin from '@backstage/plugin-devtools/alpha'; +import { unprocessedEntitiesDevToolsRoute } from '@backstage/plugin-catalog-unprocessed-entities/alpha'; /* @@ -113,7 +115,7 @@ const customHomePageModule = createFrontendModule({ const notFoundErrorPageModule = createFrontendModule({ pluginId: 'app', - extensions: [notFoundErrorPage], + extensions: [notFoundErrorPage, unprocessedEntitiesDevToolsRoute], }); const collectedLegacyPlugins = convertLegacyAppRoot( @@ -133,6 +135,7 @@ const app = createApp({ notFoundErrorPageModule, appModuleNav, customHomePageModule, + devtoolsPlugin, ...collectedLegacyPlugins, ], advanced: { diff --git a/packages/backend/package.json b/packages/backend/package.json index a60b6d3940..0d03b12d19 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -71,5 +71,8 @@ }, "devDependencies": { "@backstage/cli": "workspace:^" + }, + "volta": { + "node": "22.19.0" } } diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index 004eb96208..c48f290d25 100644 --- a/plugins/catalog-unprocessed-entities/package.json +++ b/plugins/catalog-unprocessed-entities/package.json @@ -56,6 +56,7 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/plugin-devtools": "workspace:^", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.60", diff --git a/plugins/catalog-unprocessed-entities/src/alpha/devToolsRoute.ts b/plugins/catalog-unprocessed-entities/src/alpha/devToolsRoute.ts new file mode 100644 index 0000000000..c90a6b8270 --- /dev/null +++ b/plugins/catalog-unprocessed-entities/src/alpha/devToolsRoute.ts @@ -0,0 +1,32 @@ +/* + * 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 { DevToolsRouteBlueprint } from '@backstage/plugin-devtools/alpha'; + +/** + * DevTools route extension for catalog unprocessed entities + * This demonstrates how to create extensions at the app level using the new DevTools extension system + */ +export const unprocessedEntitiesDevToolsRoute = DevToolsRouteBlueprint.make({ + params: { + path: 'unprocessed-entities', + title: 'Unprocessed Entities', + loader: () => + import('../components/UnprocessedEntities').then(m => ({ + default: m.UnprocessedEntitiesContent, + })), + }, +}); diff --git a/plugins/catalog-unprocessed-entities/src/alpha/index.ts b/plugins/catalog-unprocessed-entities/src/alpha/index.ts index 2f137f09ee..5738bb8a59 100644 --- a/plugins/catalog-unprocessed-entities/src/alpha/index.ts +++ b/plugins/catalog-unprocessed-entities/src/alpha/index.ts @@ -15,3 +15,4 @@ */ export { default } from './plugin'; +export { unprocessedEntitiesDevToolsRoute } from './devToolsRoute'; diff --git a/plugins/devtools/src/alpha/devToolsRouteBlueprint.tsx b/plugins/devtools/src/alpha/devToolsRouteBlueprint.tsx new file mode 100644 index 0000000000..fec93854ae --- /dev/null +++ b/plugins/devtools/src/alpha/devToolsRouteBlueprint.tsx @@ -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' + ? Loading...}> + : Component; + + return [ + devToolsRouteDataRef({ + path: params.path, + title: params.title, + children, + }), + ]; + }, + dataRefs: { + route: devToolsRouteDataRef, + }, +}); \ No newline at end of file diff --git a/plugins/devtools/src/alpha/devToolsRouteDataRef.ts b/plugins/devtools/src/alpha/devToolsRouteDataRef.ts new file mode 100644 index 0000000000..f4533f13e5 --- /dev/null +++ b/plugins/devtools/src/alpha/devToolsRouteDataRef.ts @@ -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() + .with({ + id: 'devtools.route', + }); \ No newline at end of file diff --git a/plugins/devtools/src/alpha/index.ts b/plugins/devtools/src/alpha/index.ts index 2f137f09ee..d326cac1be 100644 --- a/plugins/devtools/src/alpha/index.ts +++ b/plugins/devtools/src/alpha/index.ts @@ -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'; diff --git a/plugins/devtools/src/alpha/plugin.tsx b/plugins/devtools/src/alpha/plugin.tsx index 0f6d9ec4a1..02dca0141a 100644 --- a/plugins/devtools/src/alpha/plugin.tsx +++ b/plugins/devtools/src/alpha/plugin.tsx @@ -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(), - ), +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( route.get(devToolsRouteDataRef))} />), + ), + }); }, }); diff --git a/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx index b4e5d3db5b..6375e562f4 100644 --- a/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx +++ b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx @@ -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) => ( @@ -37,5 +42,14 @@ export const DefaultDevToolsPage = () => ( + {extensionRoutes?.map((route, index) => ( + + {route.children} + + ))} ); diff --git a/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx index 07a549f8eb..6c351bbf96 100644 --- a/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx +++ b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx @@ -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 || }; + return <>{outlet || }; }; diff --git a/yarn.lock b/yarn.lock index 853749b99e..b90990c0b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4938,6 +4938,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/plugin-devtools": "workspace:^" "@material-ui/core": "npm:^4.9.13" "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:^4.0.0-alpha.60" @@ -29922,6 +29923,7 @@ __metadata: "@backstage/plugin-catalog-import": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-catalog-unprocessed-entities": "workspace:^" + "@backstage/plugin-devtools": "workspace:^" "@backstage/plugin-home": "workspace:^" "@backstage/plugin-kubernetes": "workspace:^" "@backstage/plugin-kubernetes-cluster": "workspace:^"