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} + + )} ); };