Move user settings NFS tabs to subpages

Switch the new frontend system settings page to built-in subpages so the top-level page tabs come from the app shell, while keeping the legacy settings layout for the old frontend system.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-07 22:26:23 +01:00
parent f4a1edd2b0
commit f19392de18
5 changed files with 60 additions and 206 deletions
+57 -10
View File
@@ -19,13 +19,38 @@ import {
createFrontendPlugin,
PageBlueprint,
NavItemBlueprint,
SubPageBlueprint,
} from '@backstage/frontend-plugin-api';
import { Content } from '@backstage/core-components';
import SettingsIcon from '@material-ui/icons/Settings';
import { settingsRouteRef } from './plugin';
export * from './translation';
const userSettingsPage = PageBlueprint.makeWithOverrides({
const userSettingsPage = PageBlueprint.make({
params: {
path: '/settings',
routeRef: settingsRouteRef,
title: 'Settings',
},
});
const generalSettingsPage = SubPageBlueprint.make({
name: 'general',
params: {
path: 'general',
title: 'General',
loader: () =>
import('./components/General').then(m => (
<Content>
<m.UserSettingsGeneral />
</Content>
)),
},
});
const authProvidersSettingsPage = SubPageBlueprint.makeWithOverrides({
name: 'auth-providers',
inputs: {
providerSettings: createExtensionInput([coreExtensionData.reactElement], {
singleton: true,
@@ -34,20 +59,36 @@ const userSettingsPage = PageBlueprint.makeWithOverrides({
},
factory(originalFactory, { inputs }) {
return originalFactory({
path: '/settings',
routeRef: settingsRouteRef,
path: 'auth-providers',
title: 'Authentication Providers',
loader: () =>
import('./components/SettingsPage/SettingsPage').then(m => (
<m.NfsSettingsPage
providerSettings={inputs.providerSettings?.get(
coreExtensionData.reactElement,
)}
/>
import('./components/AuthProviders').then(m => (
<Content>
<m.UserSettingsAuthProviders
providerSettings={inputs.providerSettings?.get(
coreExtensionData.reactElement,
)}
/>
</Content>
)),
});
},
});
const featureFlagsSettingsPage = SubPageBlueprint.make({
name: 'feature-flags',
params: {
path: 'feature-flags',
title: 'Feature Flags',
loader: () =>
import('./components/FeatureFlags').then(m => (
<Content>
<m.UserSettingsFeatureFlags />
</Content>
)),
},
});
/** @alpha */
export const settingsNavItem = NavItemBlueprint.make({
params: {
@@ -65,7 +106,13 @@ export default createFrontendPlugin({
title: 'Settings',
icon: <SettingsIcon fontSize="inherit" />,
info: { packageJson: () => import('../package.json') },
extensions: [userSettingsPage, settingsNavItem],
extensions: [
userSettingsPage,
generalSettingsPage,
authProvidersSettingsPage,
featureFlagsSettingsPage,
settingsNavItem,
],
routes: {
root: settingsRouteRef,
},
@@ -19,10 +19,7 @@ import { UserSettingsAuthProviders } from '../AuthProviders';
import { UserSettingsFeatureFlags } from '../FeatureFlags';
import { UserSettingsGeneral } from '../General';
import { SettingsLayoutRouteProps } from '../SettingsLayout';
import {
NfsSettingsLayout,
SettingsLayout,
} from '../SettingsLayout/SettingsLayout';
import { SettingsLayout } from '../SettingsLayout/SettingsLayout';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { userSettingsTranslationRef } from '../../translation';
@@ -60,35 +57,3 @@ export const DefaultSettingsPage = (props: {
</SettingsLayout>
);
};
export const NfsDefaultSettingsPage = (props: {
tabs?: ReactElement<SettingsLayoutRouteProps>[];
providerSettings?: JSX.Element;
}) => {
const { providerSettings, tabs } = props;
const { t } = useTranslationRef(userSettingsTranslationRef);
return (
<NfsSettingsLayout>
<SettingsLayout.Route
path="general"
title={t('defaultSettingsPage.tabsTitle.general')}
>
<UserSettingsGeneral />
</SettingsLayout.Route>
<SettingsLayout.Route
path="auth-providers"
title={t('defaultSettingsPage.tabsTitle.authProviders')}
>
<UserSettingsAuthProviders providerSettings={providerSettings} />
</SettingsLayout.Route>
<SettingsLayout.Route
path="feature-flags"
title={t('defaultSettingsPage.tabsTitle.featureFlags')}
>
<UserSettingsFeatureFlags />
</SettingsLayout.Route>
{tabs}
</NfsSettingsLayout>
);
};
@@ -14,28 +14,19 @@
* limitations under the License.
*/
import { ElementType, ReactNode, useMemo } from 'react';
import { ElementType, ReactNode } from 'react';
import { TabProps } from '@material-ui/core/Tab';
import {
Content,
Header,
Page,
RoutedTabs,
useSidebarPinState,
} from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import {
attachComponentData,
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 */
@@ -62,78 +53,6 @@ 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
*/
@@ -161,50 +80,6 @@ export const SettingsLayout = (props: SettingsLayoutProps) => {
);
};
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
.selectByComponentData({
key: LAYOUT_ROUTE_DATA_KEY,
withStrictError:
'Child of SettingsLayout must be an SettingsLayout.Route',
})
.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 tabs={tabs} />}
{isMobile && <RoutedTabs routes={routes} />}
{!isMobile && (
<Content>
<Helmet title={route?.title} />
{element}
</Content>
)}
</>
);
};
attachComponentData(SettingsLayout, LAYOUT_DATA_KEY, true);
SettingsLayout.Route = Route;
@@ -15,7 +15,6 @@
*/
import { useOutlet } from 'react-router-dom';
import { DefaultSettingsPage } from '../DefaultSettingsPage';
import { NfsDefaultSettingsPage } from '../DefaultSettingsPage/DefaultSettingsPage';
import { useElementFilter } from '@backstage/core-plugin-api';
import {
SettingsLayoutProps,
@@ -53,33 +52,3 @@ export const SettingsPage = (props: { providerSettings?: JSX.Element }) => {
</>
);
};
export const NfsSettingsPage = (props: { providerSettings?: JSX.Element }) => {
const { providerSettings } = props;
const outlet = useOutlet();
const layout = useElementFilter(outlet, elements =>
elements
.selectByComponentData({
key: LAYOUT_DATA_KEY,
})
.getElements<SettingsLayoutProps>(),
);
const tabs = useElementFilter(outlet, elements =>
elements
.selectByComponentData({
key: LAYOUT_ROUTE_DATA_KEY,
})
.getElements<SettingsLayoutRouteProps>(),
);
return (
<>
{(layout.length !== 0 && layout) || (
<NfsDefaultSettingsPage
tabs={tabs}
providerSettings={providerSettings}
/>
)}
</>
);
};
+1 -3
View File
@@ -5372,7 +5372,6 @@ __metadata:
"@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": "npm:^4.9.13"
"@material-ui/icons": "npm:^4.9.1"
@@ -5536,7 +5535,7 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-devtools-react@workspace:^, @backstage/plugin-devtools-react@workspace:plugins/devtools-react":
"@backstage/plugin-devtools-react@workspace:plugins/devtools-react":
version: 0.0.0-use.local
resolution: "@backstage/plugin-devtools-react@workspace:plugins/devtools-react"
dependencies:
@@ -5574,7 +5573,6 @@ __metadata:
"@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": "npm:^4.9.13"