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:
@@ -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.
|
||||
@@ -195,11 +195,7 @@ export const NfsApiExplorerPage = (props: DefaultApiExplorerPageProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderPage
|
||||
title={t('defaultApiExplorerPage.title')}
|
||||
subtitle={generatedSubtitle}
|
||||
customActions={headerActions}
|
||||
/>
|
||||
<HeaderPage title={generatedSubtitle} customActions={headerActions} />
|
||||
<Content>
|
||||
<ApiExplorerPageContent
|
||||
initiallySelectedFilter={initiallySelectedFilter}
|
||||
|
||||
@@ -277,8 +277,7 @@ function CatalogGraphPageContent(
|
||||
return (
|
||||
<>
|
||||
<HeaderPage
|
||||
title={t('catalogGraphPage.title')}
|
||||
subtitle={subtitle}
|
||||
title={subtitle || t('catalogGraphPage.title')}
|
||||
customActions={headerActions}
|
||||
/>
|
||||
<Content stretch className={classes.content}>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 => (
|
||||
<Content>
|
||||
<m.UnprocessedEntitiesContent />
|
||||
)),
|
||||
},
|
||||
</Content>
|
||||
)),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<Box display="inline-flex" alignItems="center" height="1em" maxWidth="100%">
|
||||
<Box
|
||||
component="span"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
>
|
||||
{entity ? <EntityDisplayName entityRef={entity} hideIcon /> : title}
|
||||
</Box>
|
||||
{entity && <FavoriteEntity entity={entity} />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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 = (
|
||||
<Box display="flex" flexDirection="column">
|
||||
{title ?? <EntityHeaderTitle />}
|
||||
{entity && (
|
||||
<Box mt={1}>
|
||||
<EntityLabels entity={entity} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
const headerSubtitle = subtitle ?? (
|
||||
<EntityHeaderSubtitle parentEntityRelations={parentEntityRelations} />
|
||||
);
|
||||
const renderedTitle = typeof title === 'string' ? title : headerTitle;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderPage
|
||||
title={headerTitle}
|
||||
subtitle={
|
||||
subtitle ?? (
|
||||
<EntityHeaderSubtitle
|
||||
parentEntityRelations={parentEntityRelations}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title={renderedTitle}
|
||||
customActions={
|
||||
entity ? (
|
||||
<EntityContextMenu
|
||||
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
|
||||
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
|
||||
contextMenuItems={contextMenuItems}
|
||||
onInspectEntity={openInspectEntityDialog}
|
||||
onUnregisterEntity={openUnregisterEntityDialog}
|
||||
/>
|
||||
<>
|
||||
<FavoriteEntity entity={entity} />
|
||||
<EntityContextMenu
|
||||
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
|
||||
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
|
||||
contextMenuItems={contextMenuItems}
|
||||
onInspectEntity={openInspectEntityDialog}
|
||||
onUnregisterEntity={openUnregisterEntityDialog}
|
||||
/>
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
{(headerSubtitle || entity) && (
|
||||
<Box mt={2}>
|
||||
{headerSubtitle}
|
||||
{entity && (
|
||||
<Box mt={headerSubtitle ? 1 : 0}>
|
||||
<EntityLabels entity={entity} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{entity && (
|
||||
<>
|
||||
<InspectEntityDialog
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtensionBlueprint,
|
||||
ExtensionBoundary,
|
||||
RouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { JSX } from 'react';
|
||||
|
||||
/**
|
||||
* Parameters for creating a DevTools route extension
|
||||
* @public
|
||||
*/
|
||||
export interface DevToolsContentBlueprintParams {
|
||||
path: string;
|
||||
title: string;
|
||||
loader: () => Promise<JSX.Element>;
|
||||
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(<m.MyDevTools />),
|
||||
* ),
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
* @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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -19,7 +19,3 @@
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export {
|
||||
type DevToolsContentBlueprintParams,
|
||||
DevToolsContentBlueprint,
|
||||
} from './devToolsContentBlueprint';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 => (
|
||||
<m.NfsDevToolsPage contents={contents} />
|
||||
));
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
export const devToolsInfoPage = SubPageBlueprint.make({
|
||||
name: 'info',
|
||||
params: {
|
||||
path: 'info',
|
||||
title: 'Info',
|
||||
loader: () =>
|
||||
import('../components/Content').then(m => (
|
||||
<Content>
|
||||
<RequirePermission permission={devToolsInfoReadPermission}>
|
||||
<m.InfoContent />
|
||||
</RequirePermission>
|
||||
</Content>
|
||||
)),
|
||||
},
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
export const devToolsConfigPage = SubPageBlueprint.make({
|
||||
name: 'config',
|
||||
params: {
|
||||
path: 'config',
|
||||
title: 'Config',
|
||||
loader: () =>
|
||||
import('../components/Content').then(m => (
|
||||
<Content>
|
||||
<RequirePermission permission={devToolsConfigReadPermission}>
|
||||
<m.ConfigContent />
|
||||
</RequirePermission>
|
||||
</Content>
|
||||
)),
|
||||
},
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
export const devToolsScheduledTasksPage = SubPageBlueprint.make({
|
||||
name: 'scheduled-tasks',
|
||||
params: {
|
||||
path: 'scheduled-tasks',
|
||||
title: 'Scheduled Tasks',
|
||||
loader: () =>
|
||||
import('../components/Content').then(m => (
|
||||
<Content>
|
||||
<RequirePermission permission={devToolsTaskSchedulerReadPermission}>
|
||||
<m.ScheduledTasksContent />
|
||||
</RequirePermission>
|
||||
</Content>
|
||||
)),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -94,5 +127,12 @@ export default createFrontendPlugin({
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
},
|
||||
extensions: [devToolsApi, devToolsPage, devToolsNavItem],
|
||||
extensions: [
|
||||
devToolsApi,
|
||||
devToolsPage,
|
||||
devToolsInfoPage,
|
||||
devToolsConfigPage,
|
||||
devToolsScheduledTasksPage,
|
||||
devToolsNavItem,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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) => (
|
||||
))}
|
||||
</DevToolsLayout>
|
||||
);
|
||||
|
||||
export const NfsDefaultDevToolsPage = ({ contents }: DevToolsPageProps) => (
|
||||
<NfsDevToolsLayout>
|
||||
<DevToolsLayout.Route path="info" title="Info">
|
||||
<RequirePermission permission={devToolsInfoReadPermission}>
|
||||
<InfoContent />
|
||||
</RequirePermission>
|
||||
</DevToolsLayout.Route>
|
||||
<DevToolsLayout.Route path="config" title="Config">
|
||||
<RequirePermission permission={devToolsConfigReadPermission}>
|
||||
<ConfigContent />
|
||||
</RequirePermission>
|
||||
</DevToolsLayout.Route>
|
||||
<DevToolsLayout.Route path="scheduled-tasks" title="Scheduled Tasks">
|
||||
<RequirePermission permission={devToolsTaskSchedulerReadPermission}>
|
||||
<ScheduledTasksContent />
|
||||
</RequirePermission>
|
||||
</DevToolsLayout.Route>
|
||||
{contents?.map((content, index) => (
|
||||
<DevToolsLayout.Route
|
||||
key={`extension-${index}`}
|
||||
path={content.path}
|
||||
title={content.title}
|
||||
>
|
||||
{content.children}
|
||||
</DevToolsLayout.Route>
|
||||
))}
|
||||
</NfsDevToolsLayout>
|
||||
);
|
||||
|
||||
@@ -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<SubRoute>()
|
||||
.map(child => child.props),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderPage title={title ?? 'Backstage DevTools'} subtitle={subtitle} />
|
||||
<RoutedTabs routes={routes} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DevToolsLayout.Route = Route;
|
||||
|
||||
@@ -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 || <DefaultDevToolsPage contents={contents} />}</>;
|
||||
};
|
||||
|
||||
export const NfsDevToolsPage = ({ contents }: DevToolsPageProps) => {
|
||||
const outlet = useOutlet();
|
||||
|
||||
return <>{outlet || <NfsDefaultDevToolsPage contents={contents} />}</>;
|
||||
};
|
||||
|
||||
@@ -235,7 +235,7 @@ function NotificationsPageContent(
|
||||
if (headerVariant === 'bui') {
|
||||
return (
|
||||
<>
|
||||
<HeaderPage title={title} subtitle={subtitle} />
|
||||
<HeaderPage title={subtitle || title} />
|
||||
{pageContent}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<HeaderPage
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
customActions={headerActions}
|
||||
/>
|
||||
<HeaderPage title={buiTitle} customActions={headerActions} />
|
||||
{pageContent}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ const NfsTechDocsPageWrapper: FC<{ children?: ReactNode }> = ({ children }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderPage title="Documentation" subtitle={generatedSubtitle} />
|
||||
<HeaderPage title={generatedSubtitle} />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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<{}>) => {
|
||||
<title>{tabTitle}</title>
|
||||
</Helmet>
|
||||
<HeaderPage
|
||||
title={
|
||||
<>
|
||||
<div>{title || skeleton}</div>
|
||||
<div>{labels}</div>
|
||||
</>
|
||||
}
|
||||
subtitle={subtitle === '' ? undefined : subtitle || skeleton}
|
||||
title={title || ''}
|
||||
customActions={
|
||||
<>
|
||||
{children}
|
||||
@@ -174,6 +169,15 @@ const NfsTechDocsReaderPageHeader = (props: PropsWithChildren<{}>) => {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{(subtitle ||
|
||||
metadataLoading ||
|
||||
entityMetadataLoading ||
|
||||
entityMetadata) && (
|
||||
<Box mt={2}>
|
||||
{subtitle !== '' ? subtitle || skeleton : null}
|
||||
{entityMetadata && <Box mt={subtitle === '' ? 0 : 1}>{labels}</Box>}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user