Align NFS headers with existing BUI page patterns

Update migrated NFS pages to use the existing HeaderPage contract instead of extending Backstage UI, and move DevTools to real subpages with the legacy DevTools content blueprint removed.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-07 20:53:52 +01:00
parent aa29b508d1
commit f4a1edd2b0
18 changed files with 245 additions and 269 deletions
@@ -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<SettingsLayoutRouteProps>()
.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 && <HeaderPage title={title ?? t('settingsLayout.title')} />}
<RoutedTabs routes={routes} />
{!isMobile && <HeaderPage tabs={tabs} />}
{isMobile && <RoutedTabs routes={routes} />}
{!isMobile && (
<Content>
<Helmet title={route?.title} />
{element}
</Content>
)}
</>
);
};