diff --git a/.changeset/remove-devtools-content-blueprint.md b/.changeset/remove-devtools-content-blueprint.md
new file mode 100644
index 0000000000..5cb5c0687e
--- /dev/null
+++ b/.changeset/remove-devtools-content-blueprint.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-catalog-unprocessed-entities': patch
+'@backstage/plugin-devtools': patch
+'@backstage/plugin-devtools-react': minor
+---
+
+Removed the deprecated `DevToolsContentBlueprint` from `@backstage/plugin-devtools-react`. DevTools pages in the new frontend system now use `SubPageBlueprint` tabs instead, and the catalog unprocessed entities alpha extension now attaches to DevTools as a subpage.
diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx
index ae20aa07a9..20a8a1a7d0 100644
--- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx
+++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx
@@ -195,11 +195,7 @@ export const NfsApiExplorerPage = (props: DefaultApiExplorerPageProps) => {
return (
<>
-
+
diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json
index e9d483970a..7ffca76ef7 100644
--- a/plugins/catalog-unprocessed-entities/package.json
+++ b/plugins/catalog-unprocessed-entities/package.json
@@ -56,7 +56,6 @@
"@backstage/errors": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-catalog-unprocessed-entities-common": "workspace:^",
- "@backstage/plugin-devtools-react": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
diff --git a/plugins/catalog-unprocessed-entities/src/alpha/devToolsContent.tsx b/plugins/catalog-unprocessed-entities/src/alpha/devToolsContent.tsx
index 3dcdc0ad6b..3d88c5299d 100644
--- a/plugins/catalog-unprocessed-entities/src/alpha/devToolsContent.tsx
+++ b/plugins/catalog-unprocessed-entities/src/alpha/devToolsContent.tsx
@@ -14,23 +14,25 @@
* limitations under the License.
*/
-import { DevToolsContentBlueprint } from '@backstage/plugin-devtools-react';
+import { SubPageBlueprint } from '@backstage/frontend-plugin-api';
+import { Content } from '@backstage/core-components';
/**
* DevTools content for catalog unprocessed entities.
*
* @alpha
*/
-export const unprocessedEntitiesDevToolsContent = DevToolsContentBlueprint.make(
- {
- disabled: true,
- params: {
- path: 'unprocessed-entities',
- title: 'Unprocessed Entities',
- loader: () =>
- import('../components/UnprocessedEntities').then(m => (
+export const unprocessedEntitiesDevToolsContent = SubPageBlueprint.make({
+ attachTo: { id: 'page:devtools', input: 'pages' },
+ name: 'unprocessed-entities',
+ params: {
+ path: 'unprocessed-entities',
+ title: 'Unprocessed Entities',
+ loader: () =>
+ import('../components/UnprocessedEntities').then(m => (
+
- )),
- },
+
+ )),
},
-);
+});
diff --git a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx
index 47b449b8f8..6278bf9590 100644
--- a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx
+++ b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx
@@ -49,7 +49,6 @@ import {
EntityRefLink,
InspectEntityDialog,
UnregisterEntityDialog,
- EntityDisplayName,
FavoriteEntity,
} from '@backstage/plugin-catalog-react';
@@ -104,25 +103,6 @@ const useStyles = makeStyles(theme => ({
},
}));
-function EntityHeaderTitle() {
- const { entity } = useAsyncEntity();
- const { kind, namespace, name } = useRouteRefParams(entityRouteRef);
- const { headerTitle: title } = headerProps(kind, namespace, name, entity);
- return (
-
-
- {entity ? : title}
-
- {entity && }
-
- );
-}
-
function EntityHeaderSubtitle(props: { parentEntityRelations?: string[] }) {
const { parentEntityRelations } = props;
const classes = useStyles();
@@ -194,6 +174,8 @@ export function EntityHeader(props: {
subtitle,
} = props;
const { entity } = useAsyncEntity();
+ const { kind, namespace, name } = useRouteRefParams(entityRouteRef);
+ const { headerTitle } = headerProps(kind, namespace, name, entity);
const location = useLocation();
const navigate = useNavigate();
@@ -250,40 +232,40 @@ export function EntityHeader(props: {
);
const inspectDialogOpen = typeof selectedInspectEntityDialogTab === 'string';
- const headerTitle = (
-
- {title ?? }
- {entity && (
-
-
-
- )}
-
+ const headerSubtitle = subtitle ?? (
+
);
+ const renderedTitle = typeof title === 'string' ? title : headerTitle;
return (
<>
- )
- }
+ title={renderedTitle}
customActions={
entity ? (
-
+ <>
+
+
+ >
) : undefined
}
/>
+ {(headerSubtitle || entity) && (
+
+ {headerSubtitle}
+ {entity && (
+
+
+
+ )}
+
+ )}
{entity && (
<>
Promise;
- routeRef?: RouteRef;
-}
-
-/**
- * Extension blueprint for creating DevTools content pages (appearing as tabs)
- *
- * @example
- * ```tsx
- * const myDevToolsContent = DevToolsContentBlueprint.make({
- * {
- * params: {
- * path: 'my-dev-tools',
- * title: 'My DevTools',
- * loader: () =>
- * import('../components/MyDevTools').then(m =>
- * compatWrapper(),
- * ),
- * },
- * },
- * });
- * ```
- * @public
- */
-export const DevToolsContentBlueprint = createExtensionBlueprint({
- kind: 'devtools-content',
- attachTo: { id: 'page:devtools', input: 'contents' },
- output: [
- coreExtensionData.reactElement,
- coreExtensionData.routePath,
- coreExtensionData.routeRef.optional(),
- coreExtensionData.title,
- ],
- config: {
- schema: {
- path: z => z.string().optional(),
- title: z => z.string().optional(),
- },
- },
- *factory(params: DevToolsContentBlueprintParams, { node, config }) {
- const path = config.path ?? params.path;
- const title = config.title ?? params.title;
-
- yield coreExtensionData.reactElement(
- ExtensionBoundary.lazy(node, params.loader),
- );
-
- yield coreExtensionData.routePath(path);
-
- yield coreExtensionData.title(title);
-
- if (params.routeRef) {
- yield coreExtensionData.routeRef(params.routeRef);
- }
- },
-});
diff --git a/plugins/devtools-react/src/index.ts b/plugins/devtools-react/src/index.ts
index 70b4915ae8..2cf60407cd 100644
--- a/plugins/devtools-react/src/index.ts
+++ b/plugins/devtools-react/src/index.ts
@@ -19,7 +19,3 @@
*
* @packageDocumentation
*/
-export {
- type DevToolsContentBlueprintParams,
- DevToolsContentBlueprint,
-} from './devToolsContentBlueprint';
diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json
index 8818992168..6b6761a879 100644
--- a/plugins/devtools/package.json
+++ b/plugins/devtools/package.json
@@ -59,7 +59,6 @@
"@backstage/errors": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-devtools-common": "workspace:^",
- "@backstage/plugin-devtools-react": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.9.13",
diff --git a/plugins/devtools/src/alpha/plugin.tsx b/plugins/devtools/src/alpha/plugin.tsx
index 93bcc0bb94..4d81d43e17 100644
--- a/plugins/devtools/src/alpha/plugin.tsx
+++ b/plugins/devtools/src/alpha/plugin.tsx
@@ -21,13 +21,19 @@ import {
ApiBlueprint,
PageBlueprint,
NavItemBlueprint,
- createExtensionInput,
- coreExtensionData,
+ SubPageBlueprint,
} from '@backstage/frontend-plugin-api';
import { devToolsApiRef, DevToolsClient } from '../api';
import BuildIcon from '@material-ui/icons/Build';
+import { Content } from '@backstage/core-components';
import { rootRouteRef } from '../routes';
+import {
+ devToolsConfigReadPermission,
+ devToolsInfoReadPermission,
+} from '@backstage/plugin-devtools-common';
+import { devToolsTaskSchedulerReadPermission } from '@backstage/plugin-devtools-common/alpha';
+import { RequirePermission } from '@backstage/plugin-permission-react';
/** @alpha */
export const devToolsApi = ApiBlueprint.make({
@@ -44,35 +50,62 @@ export const devToolsApi = ApiBlueprint.make({
});
/** @alpha */
-export const devToolsPage = PageBlueprint.makeWithOverrides({
- inputs: {
- contents: createExtensionInput(
- [
- coreExtensionData.reactElement,
- coreExtensionData.routePath,
- coreExtensionData.routeRef.optional(),
- coreExtensionData.title,
- ],
- {
- optional: true,
- },
- ),
+export const devToolsPage = PageBlueprint.make({
+ params: {
+ path: '/devtools',
+ routeRef: rootRouteRef,
+ title: 'DevTools',
},
- factory(originalFactory, { inputs }) {
- return originalFactory({
- path: '/devtools',
- routeRef: rootRouteRef,
- loader: () => {
- const contents = inputs.contents.map(content => ({
- path: content.get(coreExtensionData.routePath),
- title: content.get(coreExtensionData.title),
- children: content.get(coreExtensionData.reactElement),
- }));
- return import('../components/DevToolsPage/DevToolsPage').then(m => (
-
- ));
- },
- });
+});
+
+/** @alpha */
+export const devToolsInfoPage = SubPageBlueprint.make({
+ name: 'info',
+ params: {
+ path: 'info',
+ title: 'Info',
+ loader: () =>
+ import('../components/Content').then(m => (
+
+
+
+
+
+ )),
+ },
+});
+
+/** @alpha */
+export const devToolsConfigPage = SubPageBlueprint.make({
+ name: 'config',
+ params: {
+ path: 'config',
+ title: 'Config',
+ loader: () =>
+ import('../components/Content').then(m => (
+
+
+
+
+
+ )),
+ },
+});
+
+/** @alpha */
+export const devToolsScheduledTasksPage = SubPageBlueprint.make({
+ name: 'scheduled-tasks',
+ params: {
+ path: 'scheduled-tasks',
+ title: 'Scheduled Tasks',
+ loader: () =>
+ import('../components/Content').then(m => (
+
+
+
+
+
+ )),
},
});
@@ -94,5 +127,12 @@ export default createFrontendPlugin({
routes: {
root: rootRouteRef,
},
- extensions: [devToolsApi, devToolsPage, devToolsNavItem],
+ extensions: [
+ devToolsApi,
+ devToolsPage,
+ devToolsInfoPage,
+ devToolsConfigPage,
+ devToolsScheduledTasksPage,
+ devToolsNavItem,
+ ],
});
diff --git a/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx
index 952d180ed2..06d7c2ed4e 100644
--- a/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx
+++ b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx
@@ -21,10 +21,7 @@ import {
import { ConfigContent } from '../Content';
import { devToolsTaskSchedulerReadPermission } from '@backstage/plugin-devtools-common/alpha';
-import {
- DevToolsLayout,
- NfsDevToolsLayout,
-} from '../DevToolsLayout/DevToolsLayout';
+import { DevToolsLayout } from '../DevToolsLayout/DevToolsLayout';
import { InfoContent } from '../Content';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { ScheduledTasksContent } from '../Content/ScheduledTasksContent';
@@ -59,32 +56,3 @@ export const DefaultDevToolsPage = ({ contents }: DevToolsPageProps) => (
))}
);
-
-export const NfsDefaultDevToolsPage = ({ contents }: DevToolsPageProps) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {contents?.map((content, index) => (
-
- {content.children}
-
- ))}
-
-);
diff --git a/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx
index d56f09a1d4..7ffb6dfc73 100644
--- a/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx
+++ b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx
@@ -15,7 +15,6 @@
*/
import { Header, Page, RoutedTabs } from '@backstage/core-components';
-import { HeaderPage } from '@backstage/ui';
import {
attachComponentData,
useElementFilter,
@@ -83,28 +82,4 @@ export const DevToolsLayout = ({
);
};
-export const NfsDevToolsLayout = ({
- children,
- title,
- subtitle,
-}: DevToolsLayoutProps) => {
- const routes = useElementFilter(children, elements =>
- elements
- .selectByComponentData({
- key: dataKey,
- withStrictError:
- 'Child of DevToolsLayout must be an DevToolsLayout.Route',
- })
- .getElements()
- .map(child => child.props),
- );
-
- return (
- <>
-
-
- >
- );
-};
-
DevToolsLayout.Route = Route;
diff --git a/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx
index b31b56d81f..89305b2c1a 100644
--- a/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx
+++ b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx
@@ -16,7 +16,6 @@
import { useOutlet } from 'react-router-dom';
import { DefaultDevToolsPage } from '../DefaultDevToolsPage';
-import { NfsDefaultDevToolsPage } from '../DefaultDevToolsPage/DefaultDevToolsPage';
import { ReactElement } from 'react';
/**
@@ -40,9 +39,3 @@ export const DevToolsPage = ({ contents }: DevToolsPageProps) => {
return <>{outlet || }>;
};
-
-export const NfsDevToolsPage = ({ contents }: DevToolsPageProps) => {
- const outlet = useOutlet();
-
- return <>{outlet || }>;
-};
diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx
index d7e8281d77..61dcb15e5a 100644
--- a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx
+++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx
@@ -235,7 +235,7 @@ function NotificationsPageContent(
if (headerVariant === 'bui') {
return (
<>
-
+
{pageContent}
>
);
diff --git a/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx b/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx
index 1d301465ad..fafddb2590 100644
--- a/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx
+++ b/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx
@@ -56,13 +56,16 @@ export const ScaffolderPageLayout = (props: ScaffolderPageLayoutProps) => {
);
if (headerVariant === 'bui') {
+ let buiTitle: string | undefined;
+ if (typeof title === 'string') {
+ buiTitle = title;
+ } else if (typeof subtitle === 'string') {
+ buiTitle = subtitle;
+ }
+
return (
<>
-
+
{pageContent}
>
);
diff --git a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx
index bcbc698405..31ffe55ca3 100644
--- a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx
+++ b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx
@@ -31,7 +31,7 @@ const NfsTechDocsPageWrapper: FC<{ children?: ReactNode }> = ({ children }) => {
return (
<>
-
+
{children}
>
);
diff --git a/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx b/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx
index 04da2f0ed4..1bbf5f8673 100644
--- a/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx
+++ b/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx
@@ -17,6 +17,7 @@
import { PropsWithChildren, useEffect } from 'react';
import Helmet from 'react-helmet';
import Grid from '@material-ui/core/Grid';
+import Box from '@material-ui/core/Box';
import Skeleton from '@material-ui/lab/Skeleton';
import CodeIcon from '@material-ui/icons/Code';
import capitalize from 'lodash/capitalize';
@@ -160,13 +161,7 @@ const NfsTechDocsReaderPageHeader = (props: PropsWithChildren<{}>) => {
{tabTitle}
- {title || skeleton}
- {labels}
- >
- }
- subtitle={subtitle === '' ? undefined : subtitle || skeleton}
+ title={title || ''}
customActions={
<>
{children}
@@ -174,6 +169,15 @@ const NfsTechDocsReaderPageHeader = (props: PropsWithChildren<{}>) => {
>
}
/>
+ {(subtitle ||
+ metadataLoading ||
+ entityMetadataLoading ||
+ entityMetadata) && (
+
+ {subtitle !== '' ? subtitle || skeleton : null}
+ {entityMetadata && {labels}}
+
+ )}
>
);
};
diff --git a/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx
index 7fdf2ee420..a059be505c 100644
--- a/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx
+++ b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-import { ElementType, ReactNode } from 'react';
+import { ElementType, ReactNode, useMemo } from 'react';
import { TabProps } from '@material-ui/core/Tab';
import {
+ Content,
Header,
Page,
RoutedTabs,
@@ -28,6 +29,13 @@ import {
useElementFilter,
} from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
+import { Helmet } from 'react-helmet';
+import {
+ matchRoutes,
+ useLocation,
+ useParams,
+ useRoutes,
+} from 'react-router-dom';
import { userSettingsTranslationRef } from '../../translation';
/** @public */
@@ -54,6 +62,78 @@ export type SettingsLayoutProps = {
children?: ReactNode;
};
+const normalizePath = (path: string) =>
+ path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path;
+
+const getTabsBasePath = (
+ pathname: string,
+ routes: SettingsLayoutRouteProps[],
+) => {
+ const normalizedPathname = normalizePath(pathname);
+ const relativeRoutePaths = routes
+ .map(route => route.path.replace(/^\/+|\/+$/g, ''))
+ .filter(Boolean)
+ .sort((a, b) => b.length - a.length);
+
+ for (const routePath of relativeRoutePaths) {
+ const marker = `/${routePath}`;
+ const matchIndex = normalizedPathname.lastIndexOf(marker);
+
+ if (matchIndex === -1) {
+ continue;
+ }
+
+ const matchEndIndex = matchIndex + marker.length;
+ if (
+ matchEndIndex !== normalizedPathname.length &&
+ normalizedPathname[matchEndIndex] !== '/'
+ ) {
+ continue;
+ }
+
+ return normalizedPathname.slice(0, matchIndex) || '/';
+ }
+
+ return normalizedPathname || '/';
+};
+
+const useSelectedSubRoute = (
+ subRoutes: SettingsLayoutRouteProps[],
+): {
+ route?: SettingsLayoutRouteProps;
+ element?: JSX.Element;
+} => {
+ const params = useParams();
+
+ const routes = subRoutes.map(({ path, children }) => ({
+ caseSensitive: false,
+ path: `${path}/*`,
+ element: children,
+ }));
+
+ const sortedRoutes = routes.sort((a, b) =>
+ b.path.replace(/\/\*$/, '').localeCompare(a.path.replace(/\/\*$/, '')),
+ );
+
+ const element = useRoutes(sortedRoutes) ?? subRoutes[0]?.children;
+
+ let currentRoute = params['*'] ?? '';
+ if (!currentRoute.startsWith('/')) {
+ currentRoute = `/${currentRoute}`;
+ }
+
+ const [matchedRoute] = matchRoutes(sortedRoutes, currentRoute) ?? [];
+ const foundIndex = matchedRoute
+ ? subRoutes.findIndex(t => `${t.path}/*` === matchedRoute.route.path)
+ : 0;
+ const route = subRoutes[foundIndex === -1 ? 0 : foundIndex] ?? subRoutes[0];
+
+ return {
+ route,
+ element,
+ };
+};
+
/**
* @public
*/
@@ -85,6 +165,7 @@ export const NfsSettingsLayout = (props: SettingsLayoutProps) => {
const { title, children } = props;
const { isMobile } = useSidebarPinState();
const { t } = useTranslationRef(userSettingsTranslationRef);
+ const location = useLocation();
const routes = useElementFilter(children, elements =>
elements
@@ -96,11 +177,30 @@ export const NfsSettingsLayout = (props: SettingsLayoutProps) => {
.getElements()
.map(child => child.props),
);
+ const { route, element } = useSelectedSubRoute(routes);
+ const tabs = useMemo(() => {
+ const basePath = getTabsBasePath(location.pathname, routes);
+
+ return routes.map(subRoute => ({
+ id: subRoute.path,
+ label: subRoute.title,
+ href: subRoute.path.startsWith('/')
+ ? subRoute.path
+ : `${basePath}/${subRoute.path}`.replace(/\/{2,}/g, '/'),
+ matchStrategy: 'prefix' as const,
+ }));
+ }, [location.pathname, routes]);
return (
<>
- {!isMobile && }
-
+ {!isMobile && }
+ {isMobile && }
+ {!isMobile && (
+
+
+ {element}
+
+ )}
>
);
};