Migrate NFS pages to HeaderPage

Always render the plugin header for page blueprints and move page-level actions into the Backstage UI header pattern for affected NFS pages.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-07 12:36:16 +01:00
parent b3466f6f78
commit aa29b508d1
58 changed files with 1474 additions and 556 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/frontend-plugin-api': patch
'@backstage/plugin-app': patch
'@backstage/ui': patch
---
Pages created with `PageBlueprint` now render the plugin header by default in the new frontend system, and `HeaderPage` now supports subtitles for page-level headers.
+16
View File
@@ -0,0 +1,16 @@
---
'@backstage/plugin-api-docs': patch
'@backstage/plugin-auth': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-graph': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-catalog-unprocessed-entities': patch
'@backstage/plugin-devtools': patch
'@backstage/plugin-notifications': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-search': patch
'@backstage/plugin-techdocs': patch
'@backstage/plugin-user-settings': patch
---
New frontend system pages now use the default plugin header together with `HeaderPage` instead of the legacy core page header pattern.
@@ -75,6 +75,9 @@ export const PageBlueprint = createExtensionBlueprint({
const icon = params.icon;
const pluginId = node.spec.plugin.pluginId;
const noHeader = params.noHeader ?? false;
const resolvedTitle =
title ?? node.spec.plugin.title ?? node.spec.plugin.pluginId;
const resolvedIcon = icon ?? node.spec.plugin.icon;
yield coreExtensionData.routePath(config.path ?? params.path);
if (params.loader) {
@@ -85,8 +88,8 @@ export const PageBlueprint = createExtensionBlueprint({
return (
<PageLayout
title={title ?? node.spec.plugin.title ?? node.spec.plugin.pluginId}
icon={icon ?? node.spec.plugin.icon}
title={resolvedTitle}
icon={resolvedIcon}
noHeader={noHeader}
headerActions={headerActions}
>
@@ -117,8 +120,8 @@ export const PageBlueprint = createExtensionBlueprint({
return (
<PageLayout
title={title}
icon={icon}
title={resolvedTitle}
icon={resolvedIcon}
tabs={tabs}
headerActions={headerActions}
>
@@ -147,7 +150,11 @@ export const PageBlueprint = createExtensionBlueprint({
const headerActionsApi = useApi(pluginHeaderActionsApiRef);
const headerActions = headerActionsApi.getPluginHeaderActions(pluginId);
return (
<PageLayout title={title} icon={icon} headerActions={headerActions} />
<PageLayout
title={resolvedTitle}
icon={resolvedIcon}
headerActions={headerActions}
/>
);
};
yield coreExtensionData.reactElement(<PageContent />);
+1
View File
@@ -62,6 +62,7 @@
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/ui": "workspace:^",
"@graphiql/react": "0.29.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
+7 -5
View File
@@ -77,11 +77,13 @@ const apiDocsExplorerPage = PageBlueprint.makeWithOverrides({
path: '/api-docs',
routeRef: rootRoute,
loader: () =>
import('./components/ApiExplorerPage').then(m => (
<m.ApiExplorerIndexPage
initiallySelectedFilter={config.initiallySelectedFilter}
/>
)),
import('./components/ApiExplorerPage/DefaultApiExplorerPage').then(
m => (
<m.NfsApiExplorerPage
initiallySelectedFilter={config.initiallySelectedFilter}
/>
),
),
});
},
});
@@ -24,6 +24,7 @@ import {
TableProps,
} from '@backstage/core-components';
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { HeaderPage } from '@backstage/ui';
import { CatalogTable, CatalogTableRow } from '@backstage/plugin-catalog';
import {
EntityKindPicker,
@@ -67,6 +68,42 @@ export type DefaultApiExplorerPageProps = {
pagination?: EntityListPagination;
};
type ApiExplorerPageContentProps = {
initiallySelectedFilter: UserListFilterKind;
columns?: TableColumn<CatalogTableRow>[];
actions?: TableProps<CatalogTableRow>['actions'];
ownerPickerMode?: EntityOwnerPickerProps['mode'];
pagination?: EntityListPagination;
};
function ApiExplorerPageContent(props: ApiExplorerPageContentProps) {
const {
initiallySelectedFilter,
columns,
actions,
ownerPickerMode,
pagination,
} = props;
return (
<EntityListProvider pagination={pagination}>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<EntityKindPicker initialFilter="api" hidden />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker mode={ownerPickerMode} />
<EntityLifecyclePicker />
<EntityTagPicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<CatalogTable columns={columns || defaultColumns} actions={actions} />
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
);
}
/**
* DefaultApiExplorerPage
* @public
@@ -89,6 +126,19 @@ export const DefaultApiExplorerPage = (props: DefaultApiExplorerPageProps) => {
const { allowed } = usePermission({
permission: catalogEntityCreatePermission,
});
const headerActions = (
<>
{allowed && (
<CreateButton
title={t('defaultApiExplorerPage.createButtonTitle')}
to={registerComponentLink?.()}
/>
)}
<SupportButton>
{t('defaultApiExplorerPage.supportButtonTitle')}
</SupportButton>
</>
);
return (
<PageWithHeader
@@ -98,36 +148,67 @@ export const DefaultApiExplorerPage = (props: DefaultApiExplorerPageProps) => {
pageTitleOverride={t('defaultApiExplorerPage.pageTitleOverride')}
>
<Content>
<ContentHeader title="">
{allowed && (
<CreateButton
title={t('defaultApiExplorerPage.createButtonTitle')}
to={registerComponentLink?.()}
/>
)}
<SupportButton>
{t('defaultApiExplorerPage.supportButtonTitle')}
</SupportButton>
</ContentHeader>
<EntityListProvider pagination={pagination}>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<EntityKindPicker initialFilter="api" hidden />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker mode={ownerPickerMode} />
<EntityLifecyclePicker />
<EntityTagPicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<CatalogTable
columns={columns || defaultColumns}
actions={actions}
/>
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
<ContentHeader title="">{headerActions}</ContentHeader>
<ApiExplorerPageContent
initiallySelectedFilter={initiallySelectedFilter}
columns={columns}
actions={actions}
ownerPickerMode={ownerPickerMode}
pagination={pagination}
/>
</Content>
</PageWithHeader>
);
};
export const NfsApiExplorerPage = (props: DefaultApiExplorerPageProps) => {
const {
initiallySelectedFilter = 'all',
columns,
actions,
ownerPickerMode,
pagination,
} = props;
const configApi = useApi(configApiRef);
const { t } = useTranslationRef(apiDocsTranslationRef);
const generatedSubtitle = t('defaultApiExplorerPage.subtitle', {
orgName: configApi.getOptionalString('organization.name') ?? 'Backstage',
});
const registerComponentLink = useRouteRef(registerComponentRouteRef);
const { allowed } = usePermission({
permission: catalogEntityCreatePermission,
});
const headerActions = (
<>
{allowed && (
<CreateButton
title={t('defaultApiExplorerPage.createButtonTitle')}
to={registerComponentLink?.()}
/>
)}
<SupportButton>
{t('defaultApiExplorerPage.supportButtonTitle')}
</SupportButton>
</>
);
return (
<>
<HeaderPage
title={t('defaultApiExplorerPage.title')}
subtitle={generatedSubtitle}
customActions={headerActions}
/>
<Content>
<ApiExplorerPageContent
initiallySelectedFilter={initiallySelectedFilter}
columns={columns}
actions={actions}
ownerPickerMode={ownerPickerMode}
pagination={pagination}
/>
</Content>
</>
);
};
+14 -15
View File
@@ -84,22 +84,21 @@ export const PageLayout = SwappableComponentBlueprint.make({
[tabs],
);
if (tabsWithMatchStrategy) {
return (
<>
{!noHeader && (
<PluginHeader
title={title}
icon={icon}
tabs={tabsWithMatchStrategy}
customActions={headerActions}
/>
)}
{children}
</>
);
if (noHeader) {
return <>{children}</>;
}
return <>{children}</>;
return (
<>
<PluginHeader
title={title}
icon={icon}
tabs={tabsWithMatchStrategy}
customActions={headerActions}
/>
{children}
</>
);
},
}),
});
+1
View File
@@ -49,6 +49,7 @@
"dependencies": {
"@backstage/errors": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/ui": "workspace:^",
"@remixicon/react": "^4.6.0",
"react-use": "^17.2.4"
+2 -2
View File
@@ -79,8 +79,8 @@ const CatalogGraphPage = PageBlueprint.makeWithOverrides({
path: '/catalog-graph',
routeRef: catalogGraphRouteRef,
loader: () =>
import('./components/CatalogGraphPage').then(m => (
<m.CatalogGraphPage {...config} />
import('./components/CatalogGraphPage/CatalogGraphPage').then(m => (
<m.NfsCatalogGraphPage {...config} />
)),
});
},
@@ -23,6 +23,7 @@ import {
SupportButton,
} from '@backstage/core-components';
import { useAnalytics, useRouteRef } from '@backstage/core-plugin-api';
import { HeaderPage } from '@backstage/ui';
import {
entityRouteRef,
humanizeEntityRef,
@@ -116,21 +117,23 @@ const useStyles = makeStyles(
{ name: 'PluginCatalogGraphCatalogGraphPage' },
);
export const CatalogGraphPage = (
props: {
initialState?: {
selectedRelations?: string[];
selectedKinds?: string[];
rootEntityRefs?: string[];
maxDepth?: number;
unidirectional?: boolean;
mergeRelations?: boolean;
direction?: Direction;
showFilters?: boolean;
curve?: 'curveStepBefore' | 'curveMonotoneX';
};
} & Partial<EntityRelationsGraphProps>,
) => {
type CatalogGraphPageProps = {
initialState?: {
selectedRelations?: string[];
selectedKinds?: string[];
rootEntityRefs?: string[];
maxDepth?: number;
unidirectional?: boolean;
mergeRelations?: boolean;
direction?: Direction;
showFilters?: boolean;
curve?: 'curveStepBefore' | 'curveMonotoneX';
};
} & Partial<EntityRelationsGraphProps>;
function CatalogGraphPageContent(
props: CatalogGraphPageProps & { headerVariant: 'legacy' | 'bui' },
) {
const { relationPairs, initialState, entityFilter } = props;
const { t } = useTranslationRef(catalogGraphTranslationRef);
const navigate = useNavigate();
@@ -185,93 +188,125 @@ export const CatalogGraphPage = (
[catalogEntityRoute, navigate, setRootEntityNames, analytics],
);
const pageBody = (
<Grid container alignItems="stretch" className={classes.container}>
{showFilters && (
<Grid item xs={12} lg={2} className={classes.filters}>
<MaxDepthFilter value={maxDepth} onChange={setMaxDepth} />
<SelectedKindsFilter
value={selectedKinds}
onChange={setSelectedKinds}
/>
<SelectedRelationsFilter
value={selectedRelations}
onChange={setSelectedRelations}
/>
<DirectionFilter value={direction} onChange={setDirection} />
<CurveFilter value={curve} onChange={setCurve} />
<SwitchFilter
value={unidirectional}
onChange={setUnidirectional}
label={t('catalogGraphPage.simplifiedSwitchLabel')}
/>
<SwitchFilter
value={mergeRelations}
onChange={setMergeRelations}
label={t('catalogGraphPage.mergeRelationsSwitchLabel')}
/>
</Grid>
)}
<Grid item xs className={classes.fullHeight}>
<Paper className={classes.graphWrapper}>
<Typography
variant="caption"
color="textSecondary"
display="block"
className={classes.legend}
>
<ZoomOutMap className="icon" />{' '}
{t('catalogGraphPage.zoomOutDescription')}
</Typography>
<EntityRelationsGraph
{...props}
rootEntityNames={rootEntityNames}
maxDepth={maxDepth}
kinds={
selectedKinds && selectedKinds.length > 0
? selectedKinds
: undefined
}
relations={
selectedRelations && selectedRelations.length > 0
? selectedRelations
: undefined
}
mergeRelations={mergeRelations}
unidirectional={unidirectional}
onNodeClick={onNodeClick}
direction={direction}
relationPairs={relationPairs}
entityFilter={entityFilter}
className={classes.graph}
zoom="enabled"
curve={curve}
/>
</Paper>
</Grid>
</Grid>
);
const filterAction = (
<ToggleButton
value="show filters"
selected={showFilters}
onChange={() => toggleShowFilters()}
>
<FilterListIcon /> {t('catalogGraphPage.filterToggleButtonTitle')}
</ToggleButton>
);
const headerActions = (
<>
{filterAction}
<SupportButton>
{t('catalogGraphPage.supportButtonDescription')}
</SupportButton>
</>
);
const subtitle = rootEntityNames.map(e => humanizeEntityRef(e)).join(', ');
if (props.headerVariant === 'bui') {
return (
<>
<HeaderPage
title={t('catalogGraphPage.title')}
subtitle={subtitle}
customActions={headerActions}
/>
<Content stretch className={classes.content}>
{pageBody}
</Content>
</>
);
}
return (
<Page themeId="home">
<Header
title={t('catalogGraphPage.title')}
subtitle={rootEntityNames.map(e => humanizeEntityRef(e)).join(', ')}
/>
<Header title={t('catalogGraphPage.title')} subtitle={subtitle} />
<Content stretch className={classes.content}>
<ContentHeader
titleComponent={
<ToggleButton
value="show filters"
selected={showFilters}
onChange={() => toggleShowFilters()}
>
<FilterListIcon /> {t('catalogGraphPage.filterToggleButtonTitle')}
</ToggleButton>
}
>
<ContentHeader titleComponent={filterAction}>
<SupportButton>
{t('catalogGraphPage.supportButtonDescription')}
</SupportButton>
</ContentHeader>
<Grid container alignItems="stretch" className={classes.container}>
{showFilters && (
<Grid item xs={12} lg={2} className={classes.filters}>
<MaxDepthFilter value={maxDepth} onChange={setMaxDepth} />
<SelectedKindsFilter
value={selectedKinds}
onChange={setSelectedKinds}
/>
<SelectedRelationsFilter
value={selectedRelations}
onChange={setSelectedRelations}
/>
<DirectionFilter value={direction} onChange={setDirection} />
<CurveFilter value={curve} onChange={setCurve} />
<SwitchFilter
value={unidirectional}
onChange={setUnidirectional}
label={t('catalogGraphPage.simplifiedSwitchLabel')}
/>
<SwitchFilter
value={mergeRelations}
onChange={setMergeRelations}
label={t('catalogGraphPage.mergeRelationsSwitchLabel')}
/>
</Grid>
)}
<Grid item xs className={classes.fullHeight}>
<Paper className={classes.graphWrapper}>
<Typography
variant="caption"
color="textSecondary"
display="block"
className={classes.legend}
>
<ZoomOutMap className="icon" />{' '}
{t('catalogGraphPage.zoomOutDescription')}
</Typography>
<EntityRelationsGraph
{...props}
rootEntityNames={rootEntityNames}
maxDepth={maxDepth}
kinds={
selectedKinds && selectedKinds.length > 0
? selectedKinds
: undefined
}
relations={
selectedRelations && selectedRelations.length > 0
? selectedRelations
: undefined
}
mergeRelations={mergeRelations}
unidirectional={unidirectional}
onNodeClick={onNodeClick}
direction={direction}
relationPairs={relationPairs}
entityFilter={entityFilter}
className={classes.graph}
zoom="enabled"
curve={curve}
/>
</Paper>
</Grid>
</Grid>
{pageBody}
</Content>
</Page>
);
};
}
export const CatalogGraphPage = (props: CatalogGraphPageProps) => (
<CatalogGraphPageContent {...props} headerVariant="legacy" />
);
export const NfsCatalogGraphPage = (props: CatalogGraphPageProps) => (
<CatalogGraphPageContent {...props} headerVariant="bui" />
);
+1
View File
@@ -66,6 +66,7 @@
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
+2 -2
View File
@@ -43,9 +43,9 @@ const catalogImportPage = PageBlueprint.make({
path: '/catalog-import',
routeRef: rootRouteRef,
loader: () =>
import('./components/ImportPage').then(m => (
import('./components/ImportPage/ImportPage').then(m => (
<RequirePermission permission={catalogEntityCreatePermission}>
<m.ImportPage />
<m.NfsImportPage />
</RequirePermission>
)),
},
@@ -23,6 +23,7 @@ import {
} from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { HeaderPage } from '@backstage/ui';
import Grid from '@material-ui/core/Grid';
import { useTheme } from '@material-ui/core/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';
@@ -52,17 +53,22 @@ export const DefaultImportPage = () => {
<ImportStepper />
</Grid>,
];
const headerTitle = t('defaultImportPage.headerTitle');
const supportAction = (
<SupportButton>
{t('defaultImportPage.supportTitle', { appTitle })}
</SupportButton>
);
const contentHeaderTitle = t('defaultImportPage.contentHeaderTitle', {
appTitle,
});
return (
<Page themeId="home">
<Header title={t('defaultImportPage.headerTitle')} />
<Header title={headerTitle} />
<Content>
<ContentHeader
title={t('defaultImportPage.contentHeaderTitle', { appTitle })}
>
<SupportButton>
{t('defaultImportPage.supportTitle', { appTitle })}
</SupportButton>
<ContentHeader title={contentHeaderTitle}>
{supportAction}
</ContentHeader>
<Grid container spacing={2}>
@@ -72,3 +78,39 @@ export const DefaultImportPage = () => {
</Page>
);
};
export const NfsDefaultImportPage = () => {
const { t } = useTranslationRef(catalogImportTranslationRef);
const theme = useTheme();
const configApi = useApi(configApiRef);
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const appTitle = configApi.getOptionalString('app.title') || 'Backstage';
const contentItems = [
<Grid key={0} item xs={12} md={4} lg={6} xl={8}>
<ImportInfoCard />
</Grid>,
<Grid key={1} item xs={12} md={8} lg={6} xl={4}>
<ImportStepper />
</Grid>,
];
return (
<>
<HeaderPage
title={t('defaultImportPage.contentHeaderTitle', { appTitle })}
customActions={
<SupportButton>
{t('defaultImportPage.supportTitle', { appTitle })}
</SupportButton>
}
/>
<Content>
<Grid container spacing={2}>
{isMobile ? contentItems : contentItems.reverse()}
</Grid>
</Content>
</>
);
};
@@ -16,6 +16,7 @@
import { useOutlet } from 'react-router-dom';
import { DefaultImportPage } from '../DefaultImportPage';
import { NfsDefaultImportPage } from '../DefaultImportPage/DefaultImportPage';
/**
* The whole catalog import page.
@@ -27,3 +28,9 @@ export const ImportPage = () => {
return outlet || <DefaultImportPage />;
};
export const NfsImportPage = () => {
const outlet = useOutlet();
return outlet || <NfsDefaultImportPage />;
};
@@ -57,6 +57,7 @@
"@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",
"@material-ui/lab": "^4.0.0-alpha.60",
@@ -51,7 +51,7 @@ export const catalogUnprocessedEntitiesPage = PageBlueprint.make({
routeRef: rootRouteRef,
loader: () =>
import('../components/UnprocessedEntities').then(m => (
<m.UnprocessedEntities />
<m.NfsUnprocessedEntities />
)),
},
});
@@ -16,6 +16,7 @@
import { ChangeEvent, useState } from 'react';
import { Page, Header, Content } from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import Tab from '@material-ui/core/Tab';
import { makeStyles } from '@material-ui/core/styles';
import TabContext from '@material-ui/lab/TabContext';
@@ -66,3 +67,14 @@ export const UnprocessedEntities = () => {
</Page>
);
};
export const NfsUnprocessedEntities = () => {
return (
<>
<HeaderPage title="Unprocessed Entities" />
<Content>
<UnprocessedEntitiesContent />
</Content>
</>
);
};
@@ -27,13 +27,14 @@ import useAsync from 'react-use/esm/useAsync';
import { makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';
import { Header, Breadcrumbs } from '@backstage/core-components';
import { Breadcrumbs } from '@backstage/core-components';
import {
useApi,
useRouteRef,
useRouteRefParams,
} from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/frontend-plugin-api';
import { HeaderPage } from '@backstage/ui';
import {
Entity,
@@ -57,12 +58,11 @@ import { EntityContextMenu } from '../../../components/EntityContextMenu';
import { rootRouteRef, unregisterRedirectRouteRef } from '../../../routes';
function headerProps(
paramKind: string | undefined,
_paramKind: string | undefined,
paramNamespace: string | undefined,
paramName: string | undefined,
entity: Entity | undefined,
): { headerTitle: string; headerType: string } {
const kind = paramKind ?? entity?.kind ?? '';
): { headerTitle: string } {
const namespace = paramNamespace ?? entity?.metadata.namespace ?? '';
const name =
entity?.metadata.title ?? paramName ?? entity?.metadata.name ?? '';
@@ -71,14 +71,6 @@ function headerProps(
headerTitle: `${name}${
namespace && namespace !== DEFAULT_NAMESPACE ? ` in ${namespace}` : ''
}`,
headerType: (() => {
let t = kind.toLocaleLowerCase('en-US');
if (entity && entity.spec && 'type' in entity.spec) {
t += ' — ';
t += (entity.spec as { type: string }).type.toLocaleLowerCase('en-US');
}
return t;
})(),
};
}
@@ -202,13 +194,6 @@ export function EntityHeader(props: {
subtitle,
} = props;
const { entity } = useAsyncEntity();
const { kind, namespace, name } = useRouteRefParams(entityRouteRef);
const { headerTitle: entityFallbackText, headerType: type } = headerProps(
kind,
namespace,
name,
entity,
);
const location = useLocation();
const navigate = useNavigate();
@@ -265,28 +250,42 @@ 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>
);
return (
<Header
pageTitleOverride={entityFallbackText}
type={type}
title={title ?? <EntityHeaderTitle />}
subtitle={
subtitle ?? (
<EntityHeaderSubtitle parentEntityRelations={parentEntityRelations} />
)
}
>
<>
<HeaderPage
title={headerTitle}
subtitle={
subtitle ?? (
<EntityHeaderSubtitle
parentEntityRelations={parentEntityRelations}
/>
)
}
customActions={
entity ? (
<EntityContextMenu
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
contextMenuItems={contextMenuItems}
onInspectEntity={openInspectEntityDialog}
onUnregisterEntity={openUnregisterEntityDialog}
/>
) : undefined
}
/>
{entity && (
<>
<EntityLabels entity={entity} />
<EntityContextMenu
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
contextMenuItems={contextMenuItems}
onInspectEntity={openInspectEntityDialog}
onUnregisterEntity={openUnregisterEntityDialog}
/>
<InspectEntityDialog
entity={entity!}
initialTab={
@@ -306,6 +305,6 @@ export function EntityHeader(props: {
/>
</>
)}
</Header>
</>
);
}
@@ -27,7 +27,6 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import {
Content,
Link,
Page,
Progress,
WarningPanel,
} from '@backstage/core-components';
@@ -150,7 +149,7 @@ export const EntityLayout = (props: EntityLayoutProps) => {
const { t } = useTranslationRef(catalogTranslationRef);
return (
<Page themeId={entity?.spec?.type?.toString() ?? 'home'}>
<>
{header ?? (
<EntityHeader
parentEntityRelations={parentEntityRelations}
@@ -195,7 +194,7 @@ export const EntityLayout = (props: EntityLayoutProps) => {
)}
</Content>
)}
</Page>
</>
);
};
+4 -2
View File
@@ -62,12 +62,14 @@ export const catalogPage = PageBlueprint.makeWithOverrides({
icon: <CategoryIcon fontSize="inherit" />,
title: 'Catalog',
loader: async () => {
const { BaseCatalogPage } = await import('../components/CatalogPage');
const { NfsDefaultCatalogPage } = await import(
'../components/CatalogPage/DefaultCatalogPage'
);
const filters = inputs.filters.map(filter =>
filter.get(coreExtensionData.reactElement),
);
return (
<BaseCatalogPage
<NfsDefaultCatalogPage
filters={<>{filters}</>}
pagination={config.pagination}
/>
@@ -24,6 +24,7 @@ import {
TableProps,
} from '@backstage/core-components';
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { HeaderPage } from '@backstage/ui';
import {
CatalogFilterLayout,
DefaultFilters,
@@ -48,9 +49,51 @@ export type BaseCatalogPageProps = {
pagination?: EntityListPagination;
};
function CatalogPageContent(props: BaseCatalogPageProps) {
const { filters, content = <CatalogTable />, pagination } = props;
return (
<EntityListProvider pagination={pagination}>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>{filters}</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>{content}</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
);
}
/** @internal */
export function BaseCatalogPage(props: BaseCatalogPageProps) {
const { filters, content = <CatalogTable />, pagination } = props;
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
const createComponentLink = useRouteRef(createComponentRouteRef);
const { t } = useTranslationRef(catalogTranslationRef);
const { allowed } = usePermission({
permission: catalogEntityCreatePermission,
});
const headerActions = (
<>
{allowed && (
<CreateButton
title={t('indexPage.createButtonTitle')}
to={createComponentLink && createComponentLink()}
/>
)}
<SupportButton>{t('indexPage.supportButtonContent')}</SupportButton>
</>
);
return (
<PageWithHeader title={t('indexPage.title', { orgName })} themeId="home">
<Content>
<ContentHeader title="">{headerActions}</ContentHeader>
<CatalogPageContent {...props} />
</Content>
</PageWithHeader>
);
}
function NfsBaseCatalogPage(props: BaseCatalogPageProps) {
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
const createComponentLink = useRouteRef(createComponentRouteRef);
@@ -60,25 +103,25 @@ export function BaseCatalogPage(props: BaseCatalogPageProps) {
});
return (
<PageWithHeader title={t('indexPage.title', { orgName })} themeId="home">
<>
<HeaderPage
title={t('indexPage.title', { orgName })}
customActions={
<>
{allowed && (
<CreateButton
title={t('indexPage.createButtonTitle')}
to={createComponentLink && createComponentLink()}
/>
)}
<SupportButton>{t('indexPage.supportButtonContent')}</SupportButton>
</>
}
/>
<Content>
<ContentHeader title="">
{allowed && (
<CreateButton
title={t('indexPage.createButtonTitle')}
to={createComponentLink && createComponentLink()}
/>
)}
<SupportButton>{t('indexPage.supportButtonContent')}</SupportButton>
</ContentHeader>
<EntityListProvider pagination={pagination}>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>{filters}</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>{content}</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
<CatalogPageContent {...props} />
</Content>
</PageWithHeader>
</>
);
}
@@ -138,3 +181,42 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) {
/>
);
}
export function NfsDefaultCatalogPage(props: DefaultCatalogPageProps) {
const {
columns,
actions,
initiallySelectedFilter = 'owned',
initialKind = 'component',
tableOptions = {},
emptyContent,
pagination,
ownerPickerMode,
filters,
initiallySelectedNamespaces,
} = props;
return (
<NfsBaseCatalogPage
filters={
filters ?? (
<DefaultFilters
initialKind={initialKind}
initiallySelectedFilter={initiallySelectedFilter}
ownerPickerMode={ownerPickerMode}
initiallySelectedNamespaces={initiallySelectedNamespaces}
/>
)
}
content={
<CatalogTable
columns={columns}
actions={actions}
tableOptions={tableOptions}
emptyContent={emptyContent}
/>
}
pagination={pagination}
/>
);
}
+1
View File
@@ -61,6 +61,7 @@
"@backstage/plugin-devtools-common": "workspace:^",
"@backstage/plugin-devtools-react": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.57",
+2 -2
View File
@@ -68,8 +68,8 @@ export const devToolsPage = PageBlueprint.makeWithOverrides({
title: content.get(coreExtensionData.title),
children: content.get(coreExtensionData.reactElement),
}));
return import('../components/DevToolsPage').then(m => (
<m.DevToolsPage contents={contents} />
return import('../components/DevToolsPage/DevToolsPage').then(m => (
<m.NfsDevToolsPage contents={contents} />
));
},
});
@@ -21,7 +21,10 @@ import {
import { ConfigContent } from '../Content';
import { devToolsTaskSchedulerReadPermission } from '@backstage/plugin-devtools-common/alpha';
import { DevToolsLayout } from '../DevToolsLayout';
import {
DevToolsLayout,
NfsDevToolsLayout,
} from '../DevToolsLayout/DevToolsLayout';
import { InfoContent } from '../Content';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { ScheduledTasksContent } from '../Content/ScheduledTasksContent';
@@ -56,3 +59,32 @@ 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,6 +15,7 @@
*/
import { Header, Page, RoutedTabs } from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import {
attachComponentData,
useElementFilter,
@@ -82,4 +83,28 @@ 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,6 +16,7 @@
import { useOutlet } from 'react-router-dom';
import { DefaultDevToolsPage } from '../DefaultDevToolsPage';
import { NfsDefaultDevToolsPage } from '../DefaultDevToolsPage/DefaultDevToolsPage';
import { ReactElement } from 'react';
/**
@@ -39,3 +40,9 @@ export const DevToolsPage = ({ contents }: DevToolsPageProps) => {
return <>{outlet || <DefaultDevToolsPage contents={contents} />}</>;
};
export const NfsDevToolsPage = ({ contents }: DevToolsPageProps) => {
const outlet = useOutlet();
return <>{outlet || <NfsDefaultDevToolsPage contents={contents} />}</>;
};
+1
View File
@@ -58,6 +58,7 @@
"@backstage/plugin-notifications-common": "workspace:^",
"@backstage/plugin-signals-react": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"lodash": "^4.17.21",
+1 -1
View File
@@ -30,7 +30,7 @@ const page = PageBlueprint.make({
routeRef: rootRouteRef,
loader: () =>
import('./components/NotificationsPage').then(m => (
<m.NotificationsPage />
<m.NfsNotificationsPage />
)),
},
});
@@ -21,6 +21,7 @@ import {
PageWithHeader,
ResponseErrorPanel,
} from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import Grid from '@material-ui/core/Grid';
import { ConfirmProvider } from 'material-ui-confirm';
import { useSignal } from '@backstage/plugin-signals-react';
@@ -66,7 +67,9 @@ export type NotificationsPageProps = {
typeLink?: string;
};
export const NotificationsPage = (props?: NotificationsPageProps) => {
function NotificationsPageContent(
props: NotificationsPageProps & { headerVariant: 'legacy' | 'bui' },
) {
const { t } = useTranslationRef(notificationsTranslationRef);
const {
title = t('notificationsPage.title'),
@@ -76,7 +79,8 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
type,
typeLink,
markAsReadOnLinkOpen,
} = props ?? {};
headerVariant,
} = props;
const [refresh, setRefresh] = useState(false);
const { lastSignal } = useSignal('notifications');
@@ -186,6 +190,57 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
});
}
const pageContent = (
<Content>
<ConfirmProvider>
<Grid container>
<Grid item xs={2}>
<NotificationsFilters
unreadOnly={unreadOnly}
onUnreadOnlyChanged={setUnreadOnly}
createdAfter={createdAfter}
onCreatedAfterChanged={setCreatedAfter}
onSortingChanged={setSorting}
sorting={sorting}
saved={saved}
onSavedChanged={setSaved}
severity={severity}
onSeverityChanged={setSeverity}
topic={topic}
onTopicChanged={setTopic}
allTopics={allTopics}
/>
</Grid>
<Grid item xs={10}>
<NotificationsTable
title={tableTitle}
isLoading={loading}
isUnread={isUnread}
markAsReadOnLinkOpen={markAsReadOnLinkOpen}
notifications={notifications}
onUpdate={onUpdate}
setContainsText={setContainsText}
onPageChange={setPageNumber}
onRowsPerPageChange={setPageSize}
page={pageNumber}
pageSize={pageSize}
totalCount={totalCount}
/>
</Grid>
</Grid>
</ConfirmProvider>
</Content>
);
if (headerVariant === 'bui') {
return (
<>
<HeaderPage title={title} subtitle={subtitle} />
{pageContent}
</>
);
}
return (
<PageWithHeader
title={title}
@@ -195,45 +250,15 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
type={type}
typeLink={typeLink}
>
<Content>
<ConfirmProvider>
<Grid container>
<Grid item xs={2}>
<NotificationsFilters
unreadOnly={unreadOnly}
onUnreadOnlyChanged={setUnreadOnly}
createdAfter={createdAfter}
onCreatedAfterChanged={setCreatedAfter}
onSortingChanged={setSorting}
sorting={sorting}
saved={saved}
onSavedChanged={setSaved}
severity={severity}
onSeverityChanged={setSeverity}
topic={topic}
onTopicChanged={setTopic}
allTopics={allTopics}
/>
</Grid>
<Grid item xs={10}>
<NotificationsTable
title={tableTitle}
isLoading={loading}
isUnread={isUnread}
markAsReadOnLinkOpen={markAsReadOnLinkOpen}
notifications={notifications}
onUpdate={onUpdate}
setContainsText={setContainsText}
onPageChange={setPageNumber}
onRowsPerPageChange={setPageSize}
page={pageNumber}
pageSize={pageSize}
totalCount={totalCount}
/>
</Grid>
</Grid>
</ConfirmProvider>
</Content>
{pageContent}
</PageWithHeader>
);
};
}
export const NotificationsPage = (props?: NotificationsPageProps) => (
<NotificationsPageContent {...(props ?? {})} headerVariant="legacy" />
);
export const NfsNotificationsPage = (props?: NotificationsPageProps) => (
<NotificationsPageContent {...(props ?? {})} headerVariant="bui" />
);
+1
View File
@@ -74,6 +74,7 @@
"@backstage/plugin-techdocs-common": "workspace:^",
"@backstage/plugin-techdocs-react": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/ui": "workspace:^",
"@codemirror/language": "^6.0.0",
"@codemirror/legacy-modes": "^6.1.0",
"@codemirror/view": "^6.0.0",
+2
View File
@@ -727,6 +727,7 @@ export type TemplateListPageProps = {
title?: string;
subtitle?: string;
};
headerVariant?: 'legacy' | 'bui';
};
// @alpha (undocumented)
@@ -742,6 +743,7 @@ export type TemplateWizardPageProps = {
title?: string;
subtitle?: string;
};
headerVariant?: 'legacy' | 'bui';
};
// (No @packageDocumentation comment for this package)
+9 -1
View File
@@ -499,7 +499,14 @@ export type RouterProps = {
TemplateCardComponent?: ComponentType<{
template: TemplateEntityV1beta3;
}>;
TaskPageComponent?: ComponentType<PropsWithChildren<{}>>;
TaskPageComponent?: ComponentType<
PropsWithChildren<{
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput_2;
}>;
headerVariant?: 'legacy' | 'bui';
}>
>;
EXPERIMENTAL_TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput_2;
}>;
@@ -622,6 +629,7 @@ export const TaskPage: (props: {
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput_2;
}>;
headerVariant?: 'legacy' | 'bui';
}) => JSX_2.Element;
// @public @deprecated
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Page, Header, Content } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
@@ -23,9 +22,11 @@ import { editRouteRef } from '../../../routes';
import { scaffolderTranslationRef } from '../../../translation';
import { CustomFieldExplorer } from './CustomFieldExplorer';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
interface CustomFieldsPageProps {
fieldExtensions?: FieldExtensionOptions<any, any>[];
headerVariant?: 'legacy' | 'bui';
}
export function CustomFieldsPage(props: CustomFieldsPageProps) {
@@ -33,16 +34,15 @@ export function CustomFieldsPage(props: CustomFieldsPageProps) {
const { t } = useTranslationRef(scaffolderTranslationRef);
return (
<Page themeId="home">
<Header
title={t('templateCustomFieldPage.title')}
subtitle={t('templateCustomFieldPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
/>
<Content>
<CustomFieldExplorer customFieldExtensions={props.fieldExtensions} />
</Content>
</Page>
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
title={t('templateCustomFieldPage.title')}
subtitle={t('templateCustomFieldPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
>
<CustomFieldExplorer customFieldExtensions={props.fieldExtensions} />
</ScaffolderPageLayout>
);
}
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { makeStyles } from '@material-ui/core/styles';
import { Content, Header, Page } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import {
@@ -25,6 +24,7 @@ import {
import { scaffolderTranslationRef } from '../../../translation';
import { editRouteRef } from '../../../routes';
import { TemplateEditor } from './TemplateEditor';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
const useStyles = makeStyles(
{
@@ -40,6 +40,7 @@ interface TemplatePageProps {
fieldExtensions?: FieldExtensionOptions<any, any>[];
layouts?: LayoutOptions[];
formProps?: FormProps;
headerVariant?: 'legacy' | 'bui';
}
export function TemplateEditorPage(props: TemplatePageProps) {
@@ -48,20 +49,20 @@ export function TemplateEditorPage(props: TemplatePageProps) {
const { t } = useTranslationRef(scaffolderTranslationRef);
return (
<Page themeId="home">
<Header
title={t('templateEditorPage.title')}
subtitle={t('templateEditorPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
title={t('templateEditorPage.title')}
subtitle={t('templateEditorPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
contentClassName={classes.content}
>
<TemplateEditor
layouts={props.layouts}
formProps={props.formProps}
fieldExtensions={props.fieldExtensions}
/>
<Content className={classes.content}>
<TemplateEditor
layouts={props.layouts}
formProps={props.formProps}
fieldExtensions={props.fieldExtensions}
/>
</Content>
</Page>
</ScaffolderPageLayout>
);
}
@@ -19,7 +19,6 @@ import { useNavigate } from 'react-router-dom';
import { makeStyles } from '@material-ui/core/styles';
import { Page, Header, Content } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import {
@@ -32,6 +31,7 @@ import { editRouteRef } from '../../../routes';
import { scaffolderTranslationRef } from '../../../translation';
import { TemplateFormPreviewer } from './TemplateFormPreviewer';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
const useStyles = makeStyles({
root: {
@@ -44,6 +44,7 @@ interface TemplateFormPageProps {
formProps?: FormProps;
fieldExtensions?: FieldExtensionOptions<any, any>[];
defaultPreviewTemplate?: string;
headerVariant?: 'legacy' | 'bui';
}
export function TemplateFormPage(props: TemplateFormPageProps) {
@@ -57,22 +58,22 @@ export function TemplateFormPage(props: TemplateFormPageProps) {
}, [navigate, editLink]);
return (
<Page themeId="home">
<Header
title={t('templateFormPage.title')}
subtitle={t('templateFormPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
title={t('templateFormPage.title')}
subtitle={t('templateFormPage.subtitle')}
type={t('templateIntroPage.title')}
typeLink={editLink()}
contentClassName={classes.root}
>
<TemplateFormPreviewer
layouts={props.layouts}
formProps={props.formProps}
customFieldExtensions={props.fieldExtensions}
defaultPreviewTemplate={props.defaultPreviewTemplate}
onClose={handleClose}
/>
<Content className={classes.root}>
<TemplateFormPreviewer
layouts={props.layouts}
formProps={props.formProps}
customFieldExtensions={props.fieldExtensions}
defaultPreviewTemplate={props.defaultPreviewTemplate}
onClose={handleClose}
/>
</Content>
</Page>
</ScaffolderPageLayout>
);
}
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { useCallback } from 'react';
import { Content, Header, Page } from '@backstage/core-components';
import { TemplateEditorIntro } from './TemplateEditorIntro';
import { useNavigate } from 'react-router-dom';
@@ -28,8 +27,9 @@ import {
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
import { useTemplateDirectory } from './useTemplateDirectory';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
export function TemplateIntroPage() {
export function TemplateIntroPage(props: { headerVariant?: 'legacy' | 'bui' }) {
const navigate = useNavigate();
const createLink = useRouteRef(rootRouteRef);
const editorLink = useRouteRef(editorRouteRef);
@@ -65,16 +65,15 @@ export function TemplateIntroPage() {
);
return (
<Page themeId="home">
<Header
title={t('templateIntroPage.title')}
type="Scaffolder"
typeLink={createLink()}
subtitle={t('templateIntroPage.subtitle')}
/>
<Content>
<TemplateEditorIntro onSelect={handleSelect} />
</Content>
</Page>
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
title={t('templateIntroPage.title')}
type="Scaffolder"
typeLink={createLink()}
subtitle={t('templateIntroPage.subtitle')}
>
<TemplateEditorIntro onSelect={handleSelect} />
</ScaffolderPageLayout>
);
}
@@ -20,11 +20,8 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { useApp, useRouteRef } from '@backstage/core-plugin-api';
import {
Content,
ContentHeader,
DocsIcon,
Header,
Page,
SupportButton,
} from '@backstage/core-components';
import {
@@ -59,6 +56,7 @@ import {
useTranslationRef,
} from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
import { buildTechDocsURL } from '@backstage/plugin-techdocs-react';
import {
TECHDOCS_ANNOTATION,
@@ -85,6 +83,7 @@ export type TemplateListPageProps = {
title?: string;
subtitle?: string;
};
headerVariant?: 'legacy' | 'bui';
};
const createGroupsWithOther = (
@@ -183,55 +182,62 @@ export const TemplateListPage = (props: TemplateListPageProps) => {
},
[navigate, templateRoute],
);
const pageActions = (
<>
<RegisterExistingButton
title={t('templateListPage.contentHeader.registerExistingButtonTitle')}
to={registerComponentLink && registerComponentLink()}
/>
<SupportButton>
{t('templateListPage.contentHeader.supportButtonTitle')}
</SupportButton>
</>
);
return (
<EntityListProvider>
<Page themeId="home">
<Header
pageTitleOverride={t('templateListPage.pageTitle')}
title={t('templateListPage.title')}
subtitle={t('templateListPage.subtitle')}
{...headerOptions}
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
<Content>
<ContentHeader>
<RegisterExistingButton
title={t(
'templateListPage.contentHeader.registerExistingButtonTitle',
)}
to={registerComponentLink && registerComponentLink()}
/>
<SupportButton>
{t('templateListPage.contentHeader.supportButtonTitle')}
</SupportButton>
</ContentHeader>
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
pageTitleOverride={
headerOptions?.pageTitleOverride ?? t('templateListPage.pageTitle')
}
title={headerOptions?.title ?? t('templateListPage.title')}
subtitle={headerOptions?.subtitle ?? t('templateListPage.subtitle')}
headerActions={
<>
{props.headerVariant === 'bui' ? pageActions : undefined}
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</>
}
>
{props.headerVariant !== 'bui' ? (
<ContentHeader>{pageActions}</ContentHeader>
) : null}
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<EntitySearchBar />
<EntityKindPicker initialFilter="template" hidden />
<UserListPicker
initialFilter="all"
availableFilters={['all', 'starred']}
/>
<TemplateCategoryPicker />
<EntityTagPicker />
<EntityOwnerPicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<TemplateGroups
groups={groups}
templateFilter={templateFilter}
TemplateCardComponent={TemplateCardComponent}
onTemplateSelected={onTemplateSelected}
additionalLinksForEntity={additionalLinksForEntity}
/>
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</Content>
</Page>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<EntitySearchBar />
<EntityKindPicker initialFilter="template" hidden />
<UserListPicker
initialFilter="all"
availableFilters={['all', 'starred']}
/>
<TemplateCategoryPicker />
<EntityTagPicker />
<EntityOwnerPicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<TemplateGroups
groups={groups}
templateFilter={templateFilter}
TemplateCardComponent={TemplateCardComponent}
onTemplateSelected={onTemplateSelected}
additionalLinksForEntity={additionalLinksForEntity}
/>
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</ScaffolderPageLayout>
</EntityListProvider>
);
};
@@ -41,7 +41,7 @@ import {
useTemplateParameterSchema,
} from '@backstage/plugin-scaffolder-react/alpha';
import { JsonValue } from '@backstage/types';
import { Header, Page, Progress } from '@backstage/core-components';
import { Progress } from '@backstage/core-components';
import {
rootRouteRef,
@@ -53,6 +53,7 @@ import { scaffolderTranslationRef } from '../../../translation';
import { TemplateWizardPageContextMenu } from './TemplateWizardPageContextMenu';
import { useFormDecorators } from '../../hooks';
import { ScaffolderPageLayout } from '../../../components/ScaffolderPageLayout';
/**
* @alpha
@@ -69,6 +70,7 @@ export type TemplateWizardPageProps = {
title?: string;
subtitle?: string;
};
headerVariant?: 'legacy' | 'bui';
};
export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
@@ -136,21 +138,24 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
return (
<AnalyticsContext attributes={{ entityRef: templateRef }}>
<Page themeId="website">
<Header
pageTitleOverride={
manifest?.title
? t('templateWizardPage.templateWithTitle', {
templateTitle: manifest.title,
})
: t('templateWizardPage.pageTitle')
}
title={t('templateWizardPage.title')}
subtitle={t('templateWizardPage.subtitle')}
{...props.headerOptions}
>
<TemplateWizardPageContextMenu editUrl={editUrl} />
</Header>
<ScaffolderPageLayout
themeId="website"
withContent={false}
headerVariant={props.headerVariant}
pageTitleOverride={
props.headerOptions?.pageTitleOverride ??
(manifest?.title
? t('templateWizardPage.templateWithTitle', {
templateTitle: manifest.title,
})
: t('templateWizardPage.pageTitle'))
}
title={props.headerOptions?.title ?? t('templateWizardPage.title')}
subtitle={
props.headerOptions?.subtitle ?? t('templateWizardPage.subtitle')
}
headerActions={<TemplateWizardPageContextMenu editUrl={editUrl} />}
>
{isCreating && <Progress />}
<Workflow
namespace={namespace}
@@ -162,7 +167,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
formProps={props.formProps}
layouts={props.layouts}
/>
</Page>
</ScaffolderPageLayout>
</AnalyticsContext>
);
};
+1 -1
View File
@@ -59,7 +59,7 @@ export const scaffolderPage = PageBlueprint.makeWithOverrides({
const formFields = [...apiFormFields, ...loadedFormFields];
return import('../components/Router/Router').then(m => (
<m.InternalRouter formFields={formFields} />
<m.InternalRouter formFields={formFields} headerVariant="bui" />
));
},
});
@@ -31,13 +31,10 @@ import SearchIcon from '@material-ui/icons/Search';
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
import {
Content,
EmptyState,
ErrorPanel,
Header,
Link,
MarkdownContent,
Page,
Progress,
} from '@backstage/core-components';
import { ScaffolderPageContextMenu } from '@backstage/plugin-scaffolder-react/alpha';
@@ -52,6 +49,7 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
import { Expanded, RenderSchema, SchemaRenderContext } from '../RenderSchema';
import { ScaffolderUsageExamplesTable } from '../ScaffolderUsageExamplesTable';
import { ScaffolderPageLayout } from '../ScaffolderPageLayout';
const useStyles = makeStyles(theme => ({
code: {
@@ -242,6 +240,7 @@ export type ActionsPageProps = {
create?: boolean;
templatingExtensions?: boolean;
};
headerVariant?: 'legacy' | 'bui';
};
export const ActionsPage = (props: ActionsPageProps) => {
@@ -273,17 +272,17 @@ export const ActionsPage = (props: ActionsPageProps) => {
};
return (
<Page themeId="home">
<Header
pageTitleOverride={t('actionsPage.pageTitle')}
title={t('actionsPage.title')}
subtitle={t('actionsPage.subtitle')}
>
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
pageTitleOverride={t('actionsPage.pageTitle')}
title={t('actionsPage.title')}
subtitle={t('actionsPage.subtitle')}
headerActions={
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
<Content>
<ActionPageContent />
</Content>
</Page>
}
>
<ActionPageContent />
</ScaffolderPageLayout>
);
};
@@ -14,12 +14,9 @@
* limitations under the License.
*/
import {
Content,
EmptyState,
ErrorPanel,
Header,
Link,
Page,
Progress,
Table,
} from '@backstage/core-components';
@@ -48,6 +45,7 @@ import { ScaffolderPageContextMenu } from '@backstage/plugin-scaffolder-react/al
import { useNavigate } from 'react-router-dom';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
import { ScaffolderPageLayout } from '../ScaffolderPageLayout';
export interface MyTaskPageProps {
initiallySelectedFilter?: 'owned' | 'all';
@@ -57,6 +55,7 @@ export interface MyTaskPageProps {
create?: boolean;
templatingExtensions?: boolean;
};
headerVariant?: 'legacy' | 'bui';
}
const ListTaskPageContent = (props: MyTaskPageProps) => {
@@ -200,17 +199,17 @@ export const ListTasksPage = (props: MyTaskPageProps) => {
: undefined,
};
return (
<Page themeId="home">
<Header
pageTitleOverride={t('listTaskPage.pageTitle')}
title={t('listTaskPage.title')}
subtitle={t('listTaskPage.subtitle')}
>
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
pageTitleOverride={t('listTaskPage.pageTitle')}
title={t('listTaskPage.title')}
subtitle={t('listTaskPage.subtitle')}
headerActions={
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
<Content>
<ListTaskPageContent {...props} />
</Content>
</Page>
}
>
<ListTaskPageContent {...props} />
</ScaffolderPageLayout>
);
};
@@ -20,7 +20,7 @@ import {
useMemo,
useState,
} from 'react';
import { Content, ErrorPanel, Header, Page } from '@backstage/core-components';
import { ErrorPanel } from '@backstage/core-components';
import { useNavigate, useParams } from 'react-router-dom';
import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';
@@ -57,6 +57,7 @@ import { scaffolderTranslationRef } from '../../translation';
import { entityPresentationApiRef } from '@backstage/plugin-catalog-react';
import { default as reactUseAsync } from 'react-use/esm/useAsync';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { ScaffolderPageLayout } from '../ScaffolderPageLayout';
const useStyles = makeStyles(theme => ({
contentWrapper: {
@@ -86,6 +87,7 @@ export const OngoingTask = (props: {
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput;
}>;
headerVariant?: 'legacy' | 'bui';
}) => {
// todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful.
const { taskId } = useParams();
@@ -106,9 +108,7 @@ export const OngoingTask = (props: {
taskId,
}}
>
<Page themeId="website">
<OngoingTaskContent {...props} />
</Page>
<OngoingTaskContent {...props} />
</AnalyticsContext>
);
};
@@ -117,6 +117,7 @@ function OngoingTaskContent(props: {
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput;
}>;
headerVariant?: 'legacy' | 'bui';
}) {
const { taskId } = useParams();
const templateRouteRef = useRouteRef(selectedTemplateRouteRef);
@@ -243,23 +244,24 @@ function OngoingTaskContent(props: {
!cancelEnabled || cancelStatus !== 'not-executed' || !canCancelTask;
return (
<>
<Header
pageTitleOverride={
presentation
? t('ongoingTask.pageTitle.hasTemplateName', {
templateName: presentation.primaryTitle,
})
: t('ongoingTask.pageTitle.noTemplateName')
}
title={
<div>
{t('ongoingTask.title')}{' '}
<code>{presentation ? presentation.primaryTitle : ''}</code>
</div>
}
subtitle={t('ongoingTask.subtitle', { taskId: taskId as string })}
>
<ScaffolderPageLayout
themeId="website"
headerVariant={props.headerVariant}
pageTitleOverride={
presentation
? t('ongoingTask.pageTitle.hasTemplateName', {
templateName: presentation.primaryTitle,
})
: t('ongoingTask.pageTitle.noTemplateName')
}
title={
<div>
{t('ongoingTask.title')}{' '}
<code>{presentation ? presentation.primaryTitle : ''}</code>
</div>
}
subtitle={t('ongoingTask.subtitle', { taskId: taskId as string })}
headerActions={
<ContextMenu
cancelEnabled={cancelEnabled}
canRetry={canRetry}
@@ -274,89 +276,89 @@ function OngoingTaskContent(props: {
onCancel={triggerCancel}
isCancelButtonDisabled={isCancelButtonDisabled}
/>
</Header>
<Content className={classes.contentWrapper}>
{taskStream.error ? (
<Box paddingBottom={2}>
<ErrorPanel
error={taskStream.error}
titleFormat="markdown"
title={taskStream.error.message}
/>
</Box>
) : null}
}
contentClassName={classes.contentWrapper}
>
{taskStream.error ? (
<Box paddingBottom={2}>
<TaskSteps
steps={steps}
activeStep={activeStep}
isComplete={taskStream.completed}
isError={Boolean(taskStream.error)}
<ErrorPanel
error={taskStream.error}
titleFormat="markdown"
title={taskStream.error.message}
/>
</Box>
) : null}
<Outputs output={taskStream.output} />
<Box paddingBottom={2}>
<TaskSteps
steps={steps}
activeStep={activeStep}
isComplete={taskStream.completed}
isError={Boolean(taskStream.error)}
/>
</Box>
{buttonBarVisible ? (
<Box paddingBottom={2}>
<Paper>
<Box padding={2}>
<div className={classes.buttonBar}>
<Button
className={classes.cancelButton}
disabled={
!cancelEnabled ||
(cancelStatus !== 'not-executed' && !isRetryableTask) ||
!canCancelTask
}
onClick={triggerCancel}
data-testid="cancel-button"
>
{t('ongoingTask.cancelButtonTitle')}
</Button>
{isRetryableTask && (
<Button
className={classes.retryButton}
disabled={cancelEnabled || !canRetry}
onClick={triggerRetry}
data-testid="retry-button"
>
{t('ongoingTask.retryButtonTitle')}
</Button>
)}
<Button
className={classes.logsVisibilityButton}
color="primary"
variant="outlined"
onClick={() => setLogVisibleState(!logsVisible)}
>
{logsVisible
? t('ongoingTask.hideLogsButtonTitle')
: t('ongoingTask.showLogsButtonTitle')}
</Button>
<Button
variant="contained"
color="primary"
disabled={cancelEnabled || !canStartOver}
onClick={startOver}
data-testid="start-over-button"
>
{t('ongoingTask.startOverButtonTitle')}
</Button>
</div>
</Box>
</Paper>
</Box>
) : null}
<Outputs output={taskStream.output} />
{logsVisible ? (
<Paper style={{ height: '100%' }}>
<Box padding={2} height="100%">
<TaskLogStream logs={taskStream.stepLogs} />
{buttonBarVisible ? (
<Box paddingBottom={2}>
<Paper>
<Box padding={2}>
<div className={classes.buttonBar}>
<Button
className={classes.cancelButton}
disabled={
!cancelEnabled ||
(cancelStatus !== 'not-executed' && !isRetryableTask) ||
!canCancelTask
}
onClick={triggerCancel}
data-testid="cancel-button"
>
{t('ongoingTask.cancelButtonTitle')}
</Button>
{isRetryableTask && (
<Button
className={classes.retryButton}
disabled={cancelEnabled || !canRetry}
onClick={triggerRetry}
data-testid="retry-button"
>
{t('ongoingTask.retryButtonTitle')}
</Button>
)}
<Button
className={classes.logsVisibilityButton}
color="primary"
variant="outlined"
onClick={() => setLogVisibleState(!logsVisible)}
>
{logsVisible
? t('ongoingTask.hideLogsButtonTitle')
: t('ongoingTask.showLogsButtonTitle')}
</Button>
<Button
variant="contained"
color="primary"
disabled={cancelEnabled || !canStartOver}
onClick={startOver}
data-testid="start-over-button"
>
{t('ongoingTask.startOverButtonTitle')}
</Button>
</div>
</Box>
</Paper>
) : null}
</Content>
</>
</Box>
) : null}
{logsVisible ? (
<Paper style={{ height: '100%' }}>
<Box padding={2} height="100%">
<TaskLogStream logs={taskStream.stepLogs} />
</Box>
</Paper>
) : null}
</ScaffolderPageLayout>
);
}
@@ -77,7 +77,14 @@ export type RouterProps = {
TemplateCardComponent?: ComponentType<{
template: TemplateEntityV1beta3;
}>;
TaskPageComponent?: ComponentType<PropsWithChildren<{}>>;
TaskPageComponent?: ComponentType<
PropsWithChildren<{
TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput;
}>;
headerVariant?: 'legacy' | 'bui';
}>
>;
EXPERIMENTAL_TemplateOutputsComponent?: ComponentType<{
output?: ScaffolderTaskOutput;
}>;
@@ -117,6 +124,7 @@ export const InternalRouter = (
props: PropsWithChildren<
RouterProps & {
formFields?: Array<FormField>;
headerVariant?: 'legacy' | 'bui';
}
>,
) => {
@@ -162,6 +170,7 @@ export const InternalRouter = (
groups={props.groups}
templateFilter={props.templateFilter}
headerOptions={props.headerOptions}
headerVariant={props.headerVariant}
/>
}
/>
@@ -175,6 +184,7 @@ export const InternalRouter = (
layouts={customLayouts}
components={{ ReviewStepComponent }}
formProps={props.formProps}
headerVariant={props.headerVariant}
/>
</SecretsContextProvider>
}
@@ -184,6 +194,7 @@ export const InternalRouter = (
element={
<TaskPageComponent
TemplateOutputsComponent={TemplateOutputsComponent}
headerVariant={props.headerVariant}
/>
}
/>
@@ -192,7 +203,7 @@ export const InternalRouter = (
element={
<RequirePermission permission={templateManagementPermission}>
<SecretsContextProvider>
<TemplateIntroPage />
<TemplateIntroPage headerVariant={props.headerVariant} />
</SecretsContextProvider>
</RequirePermission>
}
@@ -202,7 +213,10 @@ export const InternalRouter = (
element={
<RequirePermission permission={templateManagementPermission}>
<SecretsContextProvider>
<CustomFieldsPage fieldExtensions={fieldExtensions} />
<CustomFieldsPage
fieldExtensions={fieldExtensions}
headerVariant={props.headerVariant}
/>
</SecretsContextProvider>
</RequirePermission>
}
@@ -216,6 +230,7 @@ export const InternalRouter = (
layouts={customLayouts}
formProps={props.formProps}
fieldExtensions={fieldExtensions}
headerVariant={props.headerVariant}
/>
</SecretsContextProvider>
</RequirePermission>
@@ -224,11 +239,21 @@ export const InternalRouter = (
<Route
path={actionsRouteRef.path}
element={<ActionsPage contextMenu={props.contextMenu} />}
element={
<ActionsPage
contextMenu={props.contextMenu}
headerVariant={props.headerVariant}
/>
}
/>
<Route
path={scaffolderListTaskRouteRef.path}
element={<ListTasksPage contextMenu={props.contextMenu} />}
element={
<ListTasksPage
contextMenu={props.contextMenu}
headerVariant={props.headerVariant}
/>
}
/>
<Route
path={editorRouteRef.path}
@@ -239,6 +264,7 @@ export const InternalRouter = (
layouts={customLayouts}
formProps={props.formProps}
fieldExtensions={fieldExtensions}
headerVariant={props.headerVariant}
/>
</SecretsContextProvider>
</RequirePermission>
@@ -246,7 +272,12 @@ export const InternalRouter = (
/>
<Route
path={templatingExtensionsRouteRef.path}
element={<TemplatingExtensionsPage contextMenu={props.contextMenu} />}
element={
<TemplatingExtensionsPage
contextMenu={props.contextMenu}
headerVariant={props.headerVariant}
/>
}
/>
<Route path="*" element={<NotFoundErrorPage />} />
</Routes>
@@ -0,0 +1,85 @@
/*
* Copyright 2026 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 { Content, Header, Page } from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import type { ReactNode } from 'react';
type HeaderVariant = 'legacy' | 'bui';
type ScaffolderPageLayoutProps = {
headerVariant?: HeaderVariant;
themeId?: string;
title?: ReactNode;
subtitle?: ReactNode;
pageTitleOverride?: string;
type?: string;
typeLink?: string;
headerActions?: ReactNode;
contentClassName?: string;
withContent?: boolean;
children?: ReactNode;
};
export const ScaffolderPageLayout = (props: ScaffolderPageLayoutProps) => {
const {
headerVariant = 'legacy',
themeId = 'home',
title,
subtitle,
pageTitleOverride,
type,
typeLink,
headerActions,
contentClassName,
withContent = true,
children,
} = props;
const pageContent = withContent ? (
<Content className={contentClassName}>{children}</Content>
) : (
children
);
if (headerVariant === 'bui') {
return (
<>
<HeaderPage
title={title}
subtitle={subtitle}
customActions={headerActions}
/>
{pageContent}
</>
);
}
return (
<Page themeId={themeId}>
<Header
pageTitleOverride={pageTitleOverride}
title={title}
subtitle={subtitle}
type={type}
typeLink={typeLink}
>
{headerActions}
</Header>
{pageContent}
</Page>
);
};
@@ -29,12 +29,9 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
import {
Content,
EmptyState,
ErrorPanel,
Header,
Link,
Page,
Progress,
} from '@backstage/core-components';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
@@ -68,6 +65,7 @@ import {
TemplateGlobalFunctions,
TemplateGlobalValues,
} from './TemplateGlobals';
import { ScaffolderPageLayout } from '../ScaffolderPageLayout';
const useStyles = makeStyles(theme => ({
code: {
@@ -303,6 +301,7 @@ export type TemplatingExtensionsPageProps = {
tasks?: boolean;
create?: boolean;
};
headerVariant?: 'legacy' | 'bui';
};
export const TemplatingExtensionsPage = (
@@ -337,17 +336,17 @@ export const TemplatingExtensionsPage = (
const { t } = useTranslationRef(scaffolderTranslationRef);
return (
<Page themeId="home">
<Header
pageTitleOverride={t('templatingExtensions.pageTitle')}
title={t('templatingExtensions.title')}
subtitle={t('templatingExtensions.subtitle')}
>
<ScaffolderPageLayout
themeId="home"
headerVariant={props.headerVariant}
pageTitleOverride={t('templatingExtensions.pageTitle')}
title={t('templatingExtensions.title')}
subtitle={t('templatingExtensions.subtitle')}
headerActions={
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
<Content>
<TemplatingExtensionsPageContent linkLocal />
</Content>
</Page>
}
>
<TemplatingExtensionsPageContent linkLocal />
</ScaffolderPageLayout>
);
};
+1
View File
@@ -66,6 +66,7 @@
"@backstage/plugin-search-common": "workspace:^",
"@backstage/plugin-search-react": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/ui": "workspace:^",
"@backstage/version-bridge": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
+4 -5
View File
@@ -23,10 +23,9 @@ import {
CatalogIcon,
Content,
DocsIcon,
Header,
Page,
useSidebarPinState,
} from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import {
useApi,
discoveryApiRef,
@@ -143,8 +142,8 @@ export const searchPage = PageBlueprint.makeWithOverrides({
const configApi = useApi(configApiRef);
return (
<Page themeId="home">
{!isMobile && <Header title="Search" />}
<>
{!isMobile && <HeaderPage title="Search" />}
<Content>
<Grid container direction="row">
<Grid item xs={12}>
@@ -249,7 +248,7 @@ export const searchPage = PageBlueprint.makeWithOverrides({
</Grid>
</Grid>
</Content>
</Page>
</>
);
};
+1
View File
@@ -75,6 +75,7 @@
"@backstage/plugin-techdocs-common": "workspace:^",
"@backstage/plugin-techdocs-react": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
@@ -0,0 +1,48 @@
/*
* Copyright 2026 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 { FC, ReactNode } from 'react';
import { useOutlet } from 'react-router-dom';
import { HeaderPage } from '@backstage/ui';
import {
TechDocsIndexPageProps,
DefaultTechDocsHome,
} from '../home/components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
const NfsTechDocsPageWrapper: FC<{ children?: ReactNode }> = ({ children }) => {
const configApi = useApi(configApiRef);
const generatedSubtitle = `Documentation available in ${
configApi.getOptionalString('organization.name') ?? 'Backstage'
}`;
return (
<>
<HeaderPage title="Documentation" subtitle={generatedSubtitle} />
{children}
</>
);
};
export const NfsTechDocsIndexPage = (props: TechDocsIndexPageProps) => {
const outlet = useOutlet();
return (
outlet || (
<DefaultTechDocsHome {...props} PageWrapper={NfsTechDocsPageWrapper} />
)
);
};
@@ -0,0 +1,198 @@
/*
* Copyright 2026 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 { PropsWithChildren, useEffect } from 'react';
import Helmet from 'react-helmet';
import Grid from '@material-ui/core/Grid';
import Skeleton from '@material-ui/lab/Skeleton';
import CodeIcon from '@material-ui/icons/Code';
import capitalize from 'lodash/capitalize';
import { useParams } from 'react-router-dom';
import { HeaderLabel } from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import {
useTechDocsAddons,
TechDocsAddonLocations as locations,
useTechDocsReaderPage,
} from '@backstage/plugin-techdocs-react';
import {
entityPresentationApiRef,
EntityRefLink,
EntityRefLinks,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import {
RELATION_OWNED_BY,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { TechDocsReaderPageContent } from '../reader/components/TechDocsReaderPageContent';
import { TechDocsReaderPageSubheader } from '../reader/components/TechDocsReaderPageSubheader';
const skeleton = <Skeleton animation="wave" variant="text" height={40} />;
const NfsTechDocsReaderPageHeader = (props: PropsWithChildren<{}>) => {
const { children } = props;
const addons = useTechDocsAddons();
const configApi = useApi(configApiRef);
const entityPresentationApi = useApi(entityPresentationApiRef);
const { '*': path = '' } = useParams();
const {
title,
setTitle,
subtitle,
setSubtitle,
entityRef,
metadata: { value: metadata, loading: metadataLoading },
entityMetadata: { value: entityMetadata, loading: entityMetadataLoading },
} = useTechDocsReaderPage();
useEffect(() => {
if (!metadata) {
return;
}
setTitle(metadata.site_name);
setSubtitle(() => {
let { site_description } = metadata;
if (!site_description || site_description === 'None') {
site_description = '';
}
return site_description;
});
}, [metadata, setTitle, setSubtitle]);
const appTitle = configApi.getOptional('app.title') || 'Backstage';
const { locationMetadata, spec } = entityMetadata || {};
const lifecycle = spec?.lifecycle;
const ownedByRelations = entityMetadata
? getEntityRelations(entityMetadata, RELATION_OWNED_BY)
: [];
const labels = (
<>
<HeaderLabel
label={capitalize(entityMetadata?.kind || 'entity')}
value={
<EntityRefLink
color="inherit"
entityRef={entityRef}
title={entityMetadata?.metadata.title}
defaultKind="Component"
/>
}
/>
{ownedByRelations.length > 0 && (
<HeaderLabel
label="Owner"
value={
<EntityRefLinks
color="inherit"
entityRefs={ownedByRelations}
defaultKind="group"
/>
}
/>
)}
{lifecycle ? (
<HeaderLabel label="Lifecycle" value={String(lifecycle)} />
) : null}
{locationMetadata &&
locationMetadata.type !== 'dir' &&
locationMetadata.type !== 'file' ? (
<HeaderLabel
label=""
value={
<Grid container direction="column" alignItems="center">
<Grid style={{ padding: 0 }} item>
<CodeIcon style={{ marginTop: '-25px' }} />
</Grid>
<Grid style={{ padding: 0 }} item>
Source
</Grid>
</Grid>
}
url={locationMetadata.target}
/>
) : null}
</>
);
const noEntMetadata = !entityMetadataLoading && entityMetadata === undefined;
const noTdMetadata = !metadataLoading && metadata === undefined;
if (noEntMetadata || noTdMetadata) {
return null;
}
const stringEntityRef = stringifyEntityRef(entityRef);
const entityDisplayName =
entityPresentationApi.forEntity(stringEntityRef).snapshot.primaryTitle;
const removeTrailingSlash = (str: string) => str.replace(/\/$/, '');
const normalizeAndSpace = (str: string) =>
str.replace(/[-_]/g, ' ').split(' ').map(capitalize).join(' ');
let techdocsTabTitleItems: string[] = [];
if (path !== '') {
techdocsTabTitleItems = removeTrailingSlash(path)
.split('/')
.map(normalizeAndSpace);
}
const tabTitleItems = [entityDisplayName, ...techdocsTabTitleItems, appTitle];
const tabTitle = tabTitleItems.join(' | ');
return (
<>
<Helmet titleTemplate="%s">
<title>{tabTitle}</title>
</Helmet>
<HeaderPage
title={
<>
<div>{title || skeleton}</div>
<div>{labels}</div>
</>
}
subtitle={subtitle === '' ? undefined : subtitle || skeleton}
customActions={
<>
{children}
{addons.renderComponentsByLocation(locations.Header)}
</>
}
/>
</>
);
};
export type NfsTechDocsReaderLayoutProps = {
withHeader?: boolean;
withSearch?: boolean;
};
export const NfsTechDocsReaderLayout = (
props: NfsTechDocsReaderLayoutProps,
) => {
const { withSearch, withHeader = true } = props;
return (
<>
{withHeader && <NfsTechDocsReaderPageHeader />}
<TechDocsReaderPageSubheader />
<TechDocsReaderPageContent withSearch={withSearch} />
</>
);
};
+6 -6
View File
@@ -46,7 +46,6 @@ import {
rootDocsRouteRef,
rootRouteRef,
} from '../routes';
import { TechDocsReaderLayout } from '../reader';
import {
TechDocsAddons,
techdocsApiRef,
@@ -140,9 +139,7 @@ const techDocsPage = PageBlueprint.make({
path: '/docs',
routeRef: rootRouteRef,
loader: () =>
import('../home/components/TechDocsIndexPage').then(m => (
<m.TechDocsIndexPage />
)),
import('./NfsTechDocsIndexPage').then(m => <m.NfsTechDocsIndexPage />),
},
});
@@ -186,9 +183,12 @@ const techDocsReaderPage = PageBlueprint.makeWithOverrides({
);
});
return import('../Router').then(({ TechDocsReaderRouter }) => (
return Promise.all([
import('../Router'),
import('./NfsTechDocsReaderLayout'),
]).then(([{ TechDocsReaderRouter }, { NfsTechDocsReaderLayout }]) => (
<TechDocsReaderRouter>
<TechDocsReaderLayout
<NfsTechDocsReaderLayout
withSearch={!config.withoutSearch}
withHeader={!config.withoutHeader}
/>
@@ -140,6 +140,7 @@ export type TechDocsReaderLayoutProps = {
*/
export const TechDocsReaderLayout = (props: TechDocsReaderLayoutProps) => {
const { withSearch, withHeader = true } = props;
return (
<Page themeId="documentation">
{withHeader && <TechDocsReaderPageHeader />}
+1
View File
@@ -66,6 +66,7 @@
"@backstage/plugin-user-settings-common": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
+2 -2
View File
@@ -37,8 +37,8 @@ const userSettingsPage = PageBlueprint.makeWithOverrides({
path: '/settings',
routeRef: settingsRouteRef,
loader: () =>
import('./components/SettingsPage').then(m => (
<m.SettingsPage
import('./components/SettingsPage/SettingsPage').then(m => (
<m.NfsSettingsPage
providerSettings={inputs.providerSettings?.get(
coreExtensionData.reactElement,
)}
@@ -18,7 +18,11 @@ import { ReactElement } from 'react';
import { UserSettingsAuthProviders } from '../AuthProviders';
import { UserSettingsFeatureFlags } from '../FeatureFlags';
import { UserSettingsGeneral } from '../General';
import { SettingsLayout, SettingsLayoutRouteProps } from '../SettingsLayout';
import { SettingsLayoutRouteProps } from '../SettingsLayout';
import {
NfsSettingsLayout,
SettingsLayout,
} from '../SettingsLayout/SettingsLayout';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { userSettingsTranslationRef } from '../../translation';
@@ -56,3 +60,35 @@ 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>
);
};
@@ -22,6 +22,7 @@ import {
RoutedTabs,
useSidebarPinState,
} from '@backstage/core-components';
import { HeaderPage } from '@backstage/ui';
import {
attachComponentData,
useElementFilter,
@@ -80,6 +81,30 @@ export const SettingsLayout = (props: SettingsLayoutProps) => {
);
};
export const NfsSettingsLayout = (props: SettingsLayoutProps) => {
const { title, children } = props;
const { isMobile } = useSidebarPinState();
const { t } = useTranslationRef(userSettingsTranslationRef);
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),
);
return (
<>
{!isMobile && <HeaderPage title={title ?? t('settingsLayout.title')} />}
<RoutedTabs routes={routes} />
</>
);
};
attachComponentData(SettingsLayout, LAYOUT_DATA_KEY, true);
SettingsLayout.Route = Route;
@@ -15,6 +15,7 @@
*/
import { useOutlet } from 'react-router-dom';
import { DefaultSettingsPage } from '../DefaultSettingsPage';
import { NfsDefaultSettingsPage } from '../DefaultSettingsPage/DefaultSettingsPage';
import { useElementFilter } from '@backstage/core-plugin-api';
import {
SettingsLayoutProps,
@@ -52,3 +53,33 @@ 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}
/>
)}
</>
);
};
+59 -5
View File
@@ -3968,6 +3968,7 @@ __metadata:
"@backstage/plugin-catalog-react": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/ui": "workspace:^"
"@graphiql/react": "npm:0.29.0"
"@material-ui/core": "npm:^4.12.2"
"@material-ui/icons": "npm:^4.9.1"
@@ -4664,6 +4665,7 @@ __metadata:
"@backstage/frontend-defaults": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@backstage/ui": "workspace:^"
"@remixicon/react": "npm:^4.6.0"
"@testing-library/jest-dom": "npm:^6.0.0"
@@ -5230,6 +5232,7 @@ __metadata:
"@backstage/plugin-catalog-react": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/ui": "workspace:^"
"@material-ui/core": "npm:^4.12.2"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:4.0.0-alpha.61"
@@ -5370,6 +5373,7 @@ __metadata:
"@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"
"@material-ui/lab": "npm:^4.0.0-alpha.60"
@@ -5572,6 +5576,7 @@ __metadata:
"@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"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:^4.0.0-alpha.57"
@@ -6260,6 +6265,7 @@ __metadata:
"@backstage/plugin-signals-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@backstage/ui": "workspace:^"
"@material-ui/core": "npm:^4.9.13"
"@material-ui/icons": "npm:^4.9.1"
"@testing-library/jest-dom": "npm:^6.0.0"
@@ -7017,6 +7023,7 @@ __metadata:
"@backstage/plugin-techdocs-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
"@backstage/ui": "workspace:^"
"@codemirror/language": "npm:^6.0.0"
"@codemirror/legacy-modes": "npm:^6.1.0"
"@codemirror/view": "npm:^6.0.0"
@@ -7298,6 +7305,7 @@ __metadata:
"@backstage/plugin-search-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
"@backstage/ui": "workspace:^"
"@backstage/version-bridge": "workspace:^"
"@material-ui/core": "npm:^4.12.2"
"@material-ui/icons": "npm:^4.9.1"
@@ -7641,6 +7649,7 @@ __metadata:
"@backstage/plugin-techdocs-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@backstage/ui": "workspace:^"
"@material-ui/core": "npm:^4.12.2"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:4.0.0-alpha.61"
@@ -7724,6 +7733,7 @@ __metadata:
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@backstage/types": "workspace:^"
"@backstage/ui": "workspace:^"
"@material-ui/core": "npm:^4.12.2"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:4.0.0-alpha.61"
@@ -9686,7 +9696,17 @@ __metadata:
languageName: node
linkType: hard
"@grpc/grpc-js@npm:^1.10.9, @grpc/grpc-js@npm:^1.11.1, @grpc/grpc-js@npm:^1.14.3, @grpc/grpc-js@npm:^1.7.1":
"@grpc/grpc-js@npm:^1.10.9, @grpc/grpc-js@npm:^1.11.1, @grpc/grpc-js@npm:^1.7.1":
version: 1.12.5
resolution: "@grpc/grpc-js@npm:1.12.5"
dependencies:
"@grpc/proto-loader": "npm:^0.7.13"
"@js-sdsl/ordered-map": "npm:^4.4.2"
checksum: 10/4f8ead236dcab4d94e15e62d65ad2d93732d37f5cc52ffafe67ae00f69eae4a4c97d6d34a1b9eac9f30206468f2d15302ea6649afcba1d38929afa9d1e7c12d5
languageName: node
linkType: hard
"@grpc/grpc-js@npm:^1.14.3":
version: 1.14.3
resolution: "@grpc/grpc-js@npm:1.14.3"
dependencies:
@@ -22498,13 +22518,20 @@ __metadata:
languageName: node
linkType: hard
"@types/semver@npm:^7, @types/semver@npm:^7.1.0":
"@types/semver@npm:^7":
version: 7.7.1
resolution: "@types/semver@npm:7.7.1"
checksum: 10/8f09e7e6ca3ded67d78ba7a8f7535c8d9cf8ced83c52e7f3ac3c281fe8c689c3fe475d199d94390dc04fc681d51f2358b430bb7b2e21c62de24f2bee2c719068
languageName: node
linkType: hard
"@types/semver@npm:^7.1.0":
version: 7.7.0
resolution: "@types/semver@npm:7.7.0"
checksum: 10/ee4514c6c852b1c38f951239db02f9edeea39f5310fad9396a00b51efa2a2d96b3dfca1ae84c88181ea5b7157c57d32d7ef94edacee36fbf975546396b85ba5b
languageName: node
linkType: hard
"@types/send@npm:*, @types/send@npm:<1":
version: 0.17.6
resolution: "@types/send@npm:0.17.6"
@@ -24506,7 +24533,16 @@ __metadata:
languageName: node
linkType: hard
"acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.14.1, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.4.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0":
"acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.14.1, acorn@npm:^8.15.0, acorn@npm:^8.4.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0":
version: 8.15.0
resolution: "acorn@npm:8.15.0"
bin:
acorn: bin/acorn
checksum: 10/77f2de5051a631cf1729c090e5759148459cdb76b5f5c70f890503d629cf5052357b0ce783c0f976dd8a93c5150f59f6d18df1def3f502396a20f81282482fa4
languageName: node
linkType: hard
"acorn@npm:^8.16.0":
version: 8.16.0
resolution: "acorn@npm:8.16.0"
bin:
@@ -31447,7 +31483,7 @@ __metadata:
languageName: node
linkType: hard
"fast-xml-parser@npm:5.4.1, fast-xml-parser@npm:^5.3.4":
"fast-xml-parser@npm:5.4.1":
version: 5.4.1
resolution: "fast-xml-parser@npm:5.4.1"
dependencies:
@@ -31470,6 +31506,17 @@ __metadata:
languageName: node
linkType: hard
"fast-xml-parser@npm:^5.3.4":
version: 5.3.5
resolution: "fast-xml-parser@npm:5.3.5"
dependencies:
strnum: "npm:^2.1.2"
bin:
fxparser: src/cli/cli.js
checksum: 10/913363c2cf9ab8038bd2b666698d99d44b977725f0198f3dfff3a5d34c3109ef49d3a163a0f390f69ed00ad33b81355112dec8be5e79a13f8e6c7aaf146204b8
languageName: node
linkType: hard
"fastest-stable-stringify@npm:^2.0.2":
version: 2.0.2
resolution: "fastest-stable-stringify@npm:2.0.2"
@@ -32861,13 +32908,20 @@ __metadata:
languageName: node
linkType: hard
"globals@npm:^17.0.0, globals@npm:^17.3.0":
"globals@npm:^17.0.0":
version: 17.4.0
resolution: "globals@npm:17.4.0"
checksum: 10/ffad244617e94efcb3da72b7beefc941167c21316148ce378f322db7af72db06468f370e23224b3c7b17b5173a7c75b134e5e7b0949f2828519054a76892508d
languageName: node
linkType: hard
"globals@npm:^17.3.0":
version: 17.3.0
resolution: "globals@npm:17.3.0"
checksum: 10/44ba2b7db93eb6a2531dfba09219845e21f2e724a4f400eb59518b180b7d5bcf7f65580530e3d3023d7dc2bdbacf5d265fd87c393f567deb9a2b0472b51c9d5e
languageName: node
linkType: hard
"globalthis@npm:^1.0.1, globalthis@npm:^1.0.3, globalthis@npm:^1.0.4":
version: 1.0.4
resolution: "globalthis@npm:1.0.4"