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:
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user