From aa29b508d120e6e3cbcd9b15678ea9478348013e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 7 Mar 2026 12:36:16 +0100 Subject: [PATCH] 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 Made-with: Cursor --- .changeset/nfs-header-foundations.md | 7 + .changeset/nfs-header-page-migrations.md | 16 ++ .../src/blueprints/PageBlueprint.tsx | 17 +- plugins/api-docs/package.json | 1 + plugins/api-docs/src/alpha.tsx | 12 +- .../DefaultApiExplorerPage.tsx | 139 ++++++++--- plugins/app/src/extensions/components.tsx | 29 ++- plugins/auth/package.json | 1 + plugins/catalog-graph/src/alpha.tsx | 4 +- .../CatalogGraphPage/CatalogGraphPage.tsx | 225 ++++++++++-------- plugins/catalog-import/package.json | 1 + plugins/catalog-import/src/alpha.tsx | 4 +- .../DefaultImportPage/DefaultImportPage.tsx | 56 ++++- .../src/components/ImportPage/ImportPage.tsx | 7 + .../catalog-unprocessed-entities/package.json | 1 + .../src/alpha/plugin.tsx | 2 +- .../src/components/UnprocessedEntities.tsx | 12 + .../components/EntityHeader/EntityHeader.tsx | 75 +++--- .../components/EntityLayout/EntityLayout.tsx | 5 +- plugins/catalog/src/alpha/pages.tsx | 6 +- .../CatalogPage/DefaultCatalogPage.tsx | 118 +++++++-- plugins/devtools/package.json | 1 + plugins/devtools/src/alpha/plugin.tsx | 4 +- .../DefaultDevToolsPage.tsx | 34 ++- .../DevToolsLayout/DevToolsLayout.tsx | 25 ++ .../components/DevToolsPage/DevToolsPage.tsx | 7 + plugins/notifications/package.json | 1 + plugins/notifications/src/alpha.tsx | 2 +- .../NotificationsPage/NotificationsPage.tsx | 109 +++++---- plugins/scaffolder/package.json | 1 + plugins/scaffolder/report-alpha.api.md | 2 + plugins/scaffolder/report.api.md | 10 +- .../TemplateEditorPage/CustomFieldsPage.tsx | 24 +- .../TemplateEditorPage/TemplateEditorPage.tsx | 31 +-- .../TemplateEditorPage/TemplateFormPage.tsx | 35 +-- .../TemplateEditorPage/TemplateIntroPage.tsx | 25 +- .../TemplateListPage/TemplateListPage.tsx | 102 ++++---- .../TemplateWizardPage/TemplateWizardPage.tsx | 39 +-- plugins/scaffolder/src/alpha/extensions.tsx | 2 +- .../components/ActionsPage/ActionsPage.tsx | 27 +-- .../ListTasksPage/ListTasksPage.tsx | 27 +-- .../components/OngoingTask/OngoingTask.tsx | 196 +++++++-------- .../src/components/Router/Router.tsx | 43 +++- .../src/components/ScaffolderPageLayout.tsx | 85 +++++++ .../TemplatingExtensionsPage.tsx | 27 +-- plugins/search/package.json | 1 + plugins/search/src/alpha.tsx | 9 +- plugins/techdocs/package.json | 1 + .../src/alpha/NfsTechDocsIndexPage.tsx | 48 ++++ .../src/alpha/NfsTechDocsReaderLayout.tsx | 198 +++++++++++++++ plugins/techdocs/src/alpha/index.tsx | 12 +- .../TechDocsReaderPage/TechDocsReaderPage.tsx | 1 + plugins/user-settings/package.json | 1 + plugins/user-settings/src/alpha.tsx | 4 +- .../DefaultSettingsPage.tsx | 38 ++- .../SettingsLayout/SettingsLayout.tsx | 25 ++ .../components/SettingsPage/SettingsPage.tsx | 31 +++ yarn.lock | 64 ++++- 58 files changed, 1474 insertions(+), 556 deletions(-) create mode 100644 .changeset/nfs-header-foundations.md create mode 100644 .changeset/nfs-header-page-migrations.md create mode 100644 plugins/scaffolder/src/components/ScaffolderPageLayout.tsx create mode 100644 plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx create mode 100644 plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx diff --git a/.changeset/nfs-header-foundations.md b/.changeset/nfs-header-foundations.md new file mode 100644 index 0000000000..31d2cf5283 --- /dev/null +++ b/.changeset/nfs-header-foundations.md @@ -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. diff --git a/.changeset/nfs-header-page-migrations.md b/.changeset/nfs-header-page-migrations.md new file mode 100644 index 0000000000..f7ebfdf1b0 --- /dev/null +++ b/.changeset/nfs-header-page-migrations.md @@ -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. diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx index 513da27faa..bf2e6d8135 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -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 ( @@ -117,8 +120,8 @@ export const PageBlueprint = createExtensionBlueprint({ return ( @@ -147,7 +150,11 @@ export const PageBlueprint = createExtensionBlueprint({ const headerActionsApi = useApi(pluginHeaderActionsApiRef); const headerActions = headerActionsApi.getPluginHeaderActions(pluginId); return ( - + ); }; yield coreExtensionData.reactElement(); diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index a0d50e78ab..96248a7654 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -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", diff --git a/plugins/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx index 2724619a68..171b3bd302 100644 --- a/plugins/api-docs/src/alpha.tsx +++ b/plugins/api-docs/src/alpha.tsx @@ -77,11 +77,13 @@ const apiDocsExplorerPage = PageBlueprint.makeWithOverrides({ path: '/api-docs', routeRef: rootRoute, loader: () => - import('./components/ApiExplorerPage').then(m => ( - - )), + import('./components/ApiExplorerPage/DefaultApiExplorerPage').then( + m => ( + + ), + ), }); }, }); diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx index 625fdbe31c..ae20aa07a9 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx @@ -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[]; + actions?: TableProps['actions']; + ownerPickerMode?: EntityOwnerPickerProps['mode']; + pagination?: EntityListPagination; +}; + +function ApiExplorerPageContent(props: ApiExplorerPageContentProps) { + const { + initiallySelectedFilter, + columns, + actions, + ownerPickerMode, + pagination, + } = props; + + return ( + + + + + + + + + + ); +} + /** * DefaultApiExplorerPage * @public @@ -89,6 +126,19 @@ export const DefaultApiExplorerPage = (props: DefaultApiExplorerPageProps) => { const { allowed } = usePermission({ permission: catalogEntityCreatePermission, }); + const headerActions = ( + <> + {allowed && ( + + )} + + {t('defaultApiExplorerPage.supportButtonTitle')} + + + ); return ( { pageTitleOverride={t('defaultApiExplorerPage.pageTitleOverride')} > - - {allowed && ( - - )} - - {t('defaultApiExplorerPage.supportButtonTitle')} - - - - - - - - - - - + {headerActions} + ); }; + +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 && ( + + )} + + {t('defaultApiExplorerPage.supportButtonTitle')} + + + ); + + return ( + <> + + + + + + ); +}; diff --git a/plugins/app/src/extensions/components.tsx b/plugins/app/src/extensions/components.tsx index 230db89767..ad7ed461ac 100644 --- a/plugins/app/src/extensions/components.tsx +++ b/plugins/app/src/extensions/components.tsx @@ -84,22 +84,21 @@ export const PageLayout = SwappableComponentBlueprint.make({ [tabs], ); - if (tabsWithMatchStrategy) { - return ( - <> - {!noHeader && ( - - )} - {children} - - ); + if (noHeader) { + return <>{children}; } - return <>{children}; + + return ( + <> + + {children} + + ); }, }), }); diff --git a/plugins/auth/package.json b/plugins/auth/package.json index 52a51d81ce..d08e2c5390 100644 --- a/plugins/auth/package.json +++ b/plugins/auth/package.json @@ -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" diff --git a/plugins/catalog-graph/src/alpha.tsx b/plugins/catalog-graph/src/alpha.tsx index a2a8c7eb5e..3a004063df 100644 --- a/plugins/catalog-graph/src/alpha.tsx +++ b/plugins/catalog-graph/src/alpha.tsx @@ -79,8 +79,8 @@ const CatalogGraphPage = PageBlueprint.makeWithOverrides({ path: '/catalog-graph', routeRef: catalogGraphRouteRef, loader: () => - import('./components/CatalogGraphPage').then(m => ( - + import('./components/CatalogGraphPage/CatalogGraphPage').then(m => ( + )), }); }, diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index 7e6420da1f..f82bedd590 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -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, -) => { +type CatalogGraphPageProps = { + initialState?: { + selectedRelations?: string[]; + selectedKinds?: string[]; + rootEntityRefs?: string[]; + maxDepth?: number; + unidirectional?: boolean; + mergeRelations?: boolean; + direction?: Direction; + showFilters?: boolean; + curve?: 'curveStepBefore' | 'curveMonotoneX'; + }; +} & Partial; + +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 = ( + + {showFilters && ( + + + + + + + + + + )} + + + + {' '} + {t('catalogGraphPage.zoomOutDescription')} + + 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} + /> + + + + ); + const filterAction = ( + toggleShowFilters()} + > + {t('catalogGraphPage.filterToggleButtonTitle')} + + ); + const headerActions = ( + <> + {filterAction} + + {t('catalogGraphPage.supportButtonDescription')} + + + ); + const subtitle = rootEntityNames.map(e => humanizeEntityRef(e)).join(', '); + + if (props.headerVariant === 'bui') { + return ( + <> + + + {pageBody} + + + ); + } + return ( -
humanizeEntityRef(e)).join(', ')} - /> +
- toggleShowFilters()} - > - {t('catalogGraphPage.filterToggleButtonTitle')} - - } - > + {t('catalogGraphPage.supportButtonDescription')} - - {showFilters && ( - - - - - - - - - - )} - - - - {' '} - {t('catalogGraphPage.zoomOutDescription')} - - 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} - /> - - - + {pageBody} ); -}; +} + +export const CatalogGraphPage = (props: CatalogGraphPageProps) => ( + +); + +export const NfsCatalogGraphPage = (props: CatalogGraphPageProps) => ( + +); diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 21ec443d00..69637f30ef 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -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", diff --git a/plugins/catalog-import/src/alpha.tsx b/plugins/catalog-import/src/alpha.tsx index a00eb39988..73fc73c8df 100644 --- a/plugins/catalog-import/src/alpha.tsx +++ b/plugins/catalog-import/src/alpha.tsx @@ -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 => ( - + )), }, diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx index 24a79866b3..55d218e03b 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx @@ -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 = () => { , ]; + const headerTitle = t('defaultImportPage.headerTitle'); + const supportAction = ( + + {t('defaultImportPage.supportTitle', { appTitle })} + + ); + const contentHeaderTitle = t('defaultImportPage.contentHeaderTitle', { + appTitle, + }); return ( -
+
- - - {t('defaultImportPage.supportTitle', { appTitle })} - + + {supportAction} @@ -72,3 +78,39 @@ export const DefaultImportPage = () => { ); }; + +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 = [ + + + , + + + + , + ]; + + return ( + <> + + {t('defaultImportPage.supportTitle', { appTitle })} + + } + /> + + + {isMobile ? contentItems : contentItems.reverse()} + + + + ); +}; diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx index 4a8c16e4c7..7dbc9acdc0 100644 --- a/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx +++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx @@ -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 || ; }; + +export const NfsImportPage = () => { + const outlet = useOutlet(); + + return outlet || ; +}; diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index eea96e391e..e9d483970a 100644 --- a/plugins/catalog-unprocessed-entities/package.json +++ b/plugins/catalog-unprocessed-entities/package.json @@ -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", diff --git a/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx b/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx index d2987f3197..beeb19b153 100644 --- a/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx +++ b/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx @@ -51,7 +51,7 @@ export const catalogUnprocessedEntitiesPage = PageBlueprint.make({ routeRef: rootRouteRef, loader: () => import('../components/UnprocessedEntities').then(m => ( - + )), }, }); diff --git a/plugins/catalog-unprocessed-entities/src/components/UnprocessedEntities.tsx b/plugins/catalog-unprocessed-entities/src/components/UnprocessedEntities.tsx index 425683be25..fdbd90fb30 100644 --- a/plugins/catalog-unprocessed-entities/src/components/UnprocessedEntities.tsx +++ b/plugins/catalog-unprocessed-entities/src/components/UnprocessedEntities.tsx @@ -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 = () => { ); }; + +export const NfsUnprocessedEntities = () => { + return ( + <> + + + + + + ); +}; diff --git a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx index 75e0baa113..47b449b8f8 100644 --- a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx +++ b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx @@ -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 = ( + + {title ?? } + {entity && ( + + + + )} + + ); return ( -
} - subtitle={ - subtitle ?? ( - - ) - } - > + <> + + ) + } + customActions={ + entity ? ( + + ) : undefined + } + /> {entity && ( <> - - )} -
+ ); } diff --git a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx index 895f36150f..17c4693bde 100644 --- a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx @@ -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 ( - + <> {header ?? ( { )}
)} - + ); }; diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index 6d64c65b74..f8f09dad3b 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -62,12 +62,14 @@ export const catalogPage = PageBlueprint.makeWithOverrides({ icon: , 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 ( - {filters}} pagination={config.pagination} /> diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx index b5f0898829..d1ff7b53f4 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -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 = , pagination } = props; + + return ( + + + {filters} + {content} + + + ); +} + /** @internal */ export function BaseCatalogPage(props: BaseCatalogPageProps) { - const { filters, content = , 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 && ( + + )} + {t('indexPage.supportButtonContent')} + + ); + + return ( + + + {headerActions} + + + + ); +} + +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 ( - + <> + + {allowed && ( + + )} + {t('indexPage.supportButtonContent')} + + } + /> - - {allowed && ( - - )} - {t('indexPage.supportButtonContent')} - - - - {filters} - {content} - - + - + ); } @@ -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 ( + + ) + } + content={ + + } + pagination={pagination} + /> + ); +} diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index 36ff2b8452..8818992168 100644 --- a/plugins/devtools/package.json +++ b/plugins/devtools/package.json @@ -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", diff --git a/plugins/devtools/src/alpha/plugin.tsx b/plugins/devtools/src/alpha/plugin.tsx index b991e34bf9..93bcc0bb94 100644 --- a/plugins/devtools/src/alpha/plugin.tsx +++ b/plugins/devtools/src/alpha/plugin.tsx @@ -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 => ( - + return import('../components/DevToolsPage/DevToolsPage').then(m => ( + )); }, }); diff --git a/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx index efbed90737..952d180ed2 100644 --- a/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx +++ b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx @@ -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) => ( ))} ); + +export const NfsDefaultDevToolsPage = ({ contents }: DevToolsPageProps) => ( + + + + + + + + + + + + + + + + + {contents?.map((content, index) => ( + + {content.children} + + ))} + +); diff --git a/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx index 7ffb6dfc73..d56f09a1d4 100644 --- a/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx +++ b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx @@ -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() + .map(child => child.props), + ); + + return ( + <> + + + + ); +}; + DevToolsLayout.Route = Route; diff --git a/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx index 89305b2c1a..b31b56d81f 100644 --- a/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx +++ b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx @@ -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 || }; }; + +export const NfsDevToolsPage = ({ contents }: DevToolsPageProps) => { + const outlet = useOutlet(); + + return <>{outlet || }; +}; diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index e550a8d75e..fe443ef7f8 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -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", diff --git a/plugins/notifications/src/alpha.tsx b/plugins/notifications/src/alpha.tsx index b930373a46..9425e682b6 100644 --- a/plugins/notifications/src/alpha.tsx +++ b/plugins/notifications/src/alpha.tsx @@ -30,7 +30,7 @@ const page = PageBlueprint.make({ routeRef: rootRouteRef, loader: () => import('./components/NotificationsPage').then(m => ( - + )), }, }); diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx index 49530797c3..d7e8281d77 100644 --- a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx +++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx @@ -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 = ( + + + + + + + + + + + + + ); + + if (headerVariant === 'bui') { + return ( + <> + + {pageContent} + + ); + } + return ( { type={type} typeLink={typeLink} > - - - - - - - - - - - - + {pageContent} ); -}; +} + +export const NotificationsPage = (props?: NotificationsPageProps) => ( + +); + +export const NfsNotificationsPage = (props?: NotificationsPageProps) => ( + +); diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index b2ee8a47d4..77d35445b0 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -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", diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index b2cd1bbcbe..7ce069e2ec 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -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) diff --git a/plugins/scaffolder/report.api.md b/plugins/scaffolder/report.api.md index 17fd25e646..0f5c617acc 100644 --- a/plugins/scaffolder/report.api.md +++ b/plugins/scaffolder/report.api.md @@ -499,7 +499,14 @@ export type RouterProps = { TemplateCardComponent?: ComponentType<{ template: TemplateEntityV1beta3; }>; - TaskPageComponent?: ComponentType>; + 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 diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldsPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldsPage.tsx index d7f6525f47..a188aca2ad 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldsPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldsPage.tsx @@ -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[]; + headerVariant?: 'legacy' | 'bui'; } export function CustomFieldsPage(props: CustomFieldsPageProps) { @@ -33,16 +34,15 @@ export function CustomFieldsPage(props: CustomFieldsPageProps) { const { t } = useTranslationRef(scaffolderTranslationRef); return ( - -
- - - - + + + ); } diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorPage.tsx index 13439c666f..b94d831ba4 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorPage.tsx @@ -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[]; 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 ( - -
+ - - - - + ); } diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.tsx index 4e726fec96..adf4027e29 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.tsx @@ -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[]; defaultPreviewTemplate?: string; + headerVariant?: 'legacy' | 'bui'; } export function TemplateFormPage(props: TemplateFormPageProps) { @@ -57,22 +58,22 @@ export function TemplateFormPage(props: TemplateFormPageProps) { }, [navigate, editLink]); return ( - -
+ - - - - + ); } diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx index 04dcf9e481..6140e6849f 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx @@ -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 ( - -
- - - - + + + ); } diff --git a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.tsx index 37a50ed904..4fa884a534 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.tsx @@ -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 = ( + <> + + + {t('templateListPage.contentHeader.supportButtonTitle')} + + + ); return ( - -
- -
- - - - - {t('templateListPage.contentHeader.supportButtonTitle')} - - + + {props.headerVariant === 'bui' ? pageActions : undefined} + + + } + > + {props.headerVariant !== 'bui' ? ( + {pageActions} + ) : null} - - - - - - - - - -
+ + + + + + + + +
); }; diff --git a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.tsx index 5f0176e439..bee0b7d2f0 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.tsx @@ -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 ( - -
- -
+ } + > {isCreating && } { formProps={props.formProps} layouts={props.layouts} /> -
+
); }; diff --git a/plugins/scaffolder/src/alpha/extensions.tsx b/plugins/scaffolder/src/alpha/extensions.tsx index 91306bf5fd..08a8551ba9 100644 --- a/plugins/scaffolder/src/alpha/extensions.tsx +++ b/plugins/scaffolder/src/alpha/extensions.tsx @@ -59,7 +59,7 @@ export const scaffolderPage = PageBlueprint.makeWithOverrides({ const formFields = [...apiFormFields, ...loadedFormFields]; return import('../components/Router/Router').then(m => ( - + )); }, }); diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 6be502659a..0d32e05e65 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -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 ( - -
+ -
- - - -
+ } + > + + ); }; diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx index 6cce6c3727..58c89d7cc1 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx @@ -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 ( - -
+ -
- - - -
+ } + > + + ); }; diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx index bc0d5fe1aa..901ab1a0d0 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx @@ -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, }} > - - - + ); }; @@ -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 ( - <> -
- {t('ongoingTask.title')}{' '} - {presentation ? presentation.primaryTitle : ''} - - } - subtitle={t('ongoingTask.subtitle', { taskId: taskId as string })} - > + + {t('ongoingTask.title')}{' '} + {presentation ? presentation.primaryTitle : ''} + + } + subtitle={t('ongoingTask.subtitle', { taskId: taskId as string })} + headerActions={ -
- - {taskStream.error ? ( - - - - ) : null} - + } + contentClassName={classes.contentWrapper} + > + {taskStream.error ? ( - + ) : null} - + + + - {buttonBarVisible ? ( - - - -
- - {isRetryableTask && ( - - )} - - -
-
-
-
- ) : null} + - {logsVisible ? ( - - - + {buttonBarVisible ? ( + + + +
+ + {isRetryableTask && ( + + )} + + +
- ) : null} -
- + + ) : null} + + {logsVisible ? ( + + + + + + ) : null} + ); } diff --git a/plugins/scaffolder/src/components/Router/Router.tsx b/plugins/scaffolder/src/components/Router/Router.tsx index 0fa43901bf..81c20c96ba 100644 --- a/plugins/scaffolder/src/components/Router/Router.tsx +++ b/plugins/scaffolder/src/components/Router/Router.tsx @@ -77,7 +77,14 @@ export type RouterProps = { TemplateCardComponent?: ComponentType<{ template: TemplateEntityV1beta3; }>; - TaskPageComponent?: ComponentType>; + 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; + 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} /> } @@ -184,6 +194,7 @@ export const InternalRouter = ( element={ } /> @@ -192,7 +203,7 @@ export const InternalRouter = ( element={ - + } @@ -202,7 +213,10 @@ export const InternalRouter = ( element={ - + } @@ -216,6 +230,7 @@ export const InternalRouter = ( layouts={customLayouts} formProps={props.formProps} fieldExtensions={fieldExtensions} + headerVariant={props.headerVariant} /> @@ -224,11 +239,21 @@ export const InternalRouter = ( } + element={ + + } /> } + element={ + + } /> @@ -246,7 +272,12 @@ export const InternalRouter = ( /> } + element={ + + } /> } /> diff --git a/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx b/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx new file mode 100644 index 0000000000..1d301465ad --- /dev/null +++ b/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx @@ -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 ? ( + {children} + ) : ( + children + ); + + if (headerVariant === 'bui') { + return ( + <> + + {pageContent} + + ); + } + + return ( + +
+ {headerActions} +
+ {pageContent} +
+ ); +}; diff --git a/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplatingExtensionsPage.tsx b/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplatingExtensionsPage.tsx index fc2b180596..184c54471e 100644 --- a/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplatingExtensionsPage.tsx +++ b/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplatingExtensionsPage.tsx @@ -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 ( - -
+ -
- - - -
+ } + > + + ); }; diff --git a/plugins/search/package.json b/plugins/search/package.json index 4e731a8769..5a7cc9b478 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -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", diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 9625f5e946..3acad014a5 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -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 ( - - {!isMobile &&
} + <> + {!isMobile && } @@ -249,7 +248,7 @@ export const searchPage = PageBlueprint.makeWithOverrides({ - + ); }; diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 5ec3c79f7d..11b8631a5e 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -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", diff --git a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx new file mode 100644 index 0000000000..bcbc698405 --- /dev/null +++ b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx @@ -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 ( + <> + + {children} + + ); +}; + +export const NfsTechDocsIndexPage = (props: TechDocsIndexPageProps) => { + const outlet = useOutlet(); + + return ( + outlet || ( + + ) + ); +}; diff --git a/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx b/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx new file mode 100644 index 0000000000..04da2f0ed4 --- /dev/null +++ b/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx @@ -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 = ; + +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 = ( + <> + + } + /> + {ownedByRelations.length > 0 && ( + + } + /> + )} + {lifecycle ? ( + + ) : null} + {locationMetadata && + locationMetadata.type !== 'dir' && + locationMetadata.type !== 'file' ? ( + + + + + + Source + + + } + 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 ( + <> + + {tabTitle} + + +
{title || skeleton}
+
{labels}
+ + } + 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 && } + + + + ); +}; diff --git a/plugins/techdocs/src/alpha/index.tsx b/plugins/techdocs/src/alpha/index.tsx index 1ec92edfd3..47bedd0ce3 100644 --- a/plugins/techdocs/src/alpha/index.tsx +++ b/plugins/techdocs/src/alpha/index.tsx @@ -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 => ( - - )), + import('./NfsTechDocsIndexPage').then(m => ), }, }); @@ -186,9 +183,12 @@ const techDocsReaderPage = PageBlueprint.makeWithOverrides({ ); }); - return import('../Router').then(({ TechDocsReaderRouter }) => ( + return Promise.all([ + import('../Router'), + import('./NfsTechDocsReaderLayout'), + ]).then(([{ TechDocsReaderRouter }, { NfsTechDocsReaderLayout }]) => ( - diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index 4a2b9e2002..dbbb091412 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -140,6 +140,7 @@ export type TechDocsReaderLayoutProps = { */ export const TechDocsReaderLayout = (props: TechDocsReaderLayoutProps) => { const { withSearch, withHeader = true } = props; + return ( {withHeader && } diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index a4d5d505b7..5a10a7b3ef 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -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", diff --git a/plugins/user-settings/src/alpha.tsx b/plugins/user-settings/src/alpha.tsx index 87b858fcec..2d6327f030 100644 --- a/plugins/user-settings/src/alpha.tsx +++ b/plugins/user-settings/src/alpha.tsx @@ -37,8 +37,8 @@ const userSettingsPage = PageBlueprint.makeWithOverrides({ path: '/settings', routeRef: settingsRouteRef, loader: () => - import('./components/SettingsPage').then(m => ( - ( + ); }; + +export const NfsDefaultSettingsPage = (props: { + tabs?: ReactElement[]; + providerSettings?: JSX.Element; +}) => { + const { providerSettings, tabs } = props; + const { t } = useTranslationRef(userSettingsTranslationRef); + + return ( + + + + + + + + + + + {tabs} + + ); +}; diff --git a/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx index 76ee01e202..7fdf2ee420 100644 --- a/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx +++ b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx @@ -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() + .map(child => child.props), + ); + + return ( + <> + {!isMobile && } + + + ); +}; + attachComponentData(SettingsLayout, LAYOUT_DATA_KEY, true); SettingsLayout.Route = Route; diff --git a/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx index b72862e2bc..14797a3763 100644 --- a/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx +++ b/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx @@ -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(), + ); + const tabs = useElementFilter(outlet, elements => + elements + .selectByComponentData({ + key: LAYOUT_ROUTE_DATA_KEY, + }) + .getElements(), + ); + + return ( + <> + {(layout.length !== 0 && layout) || ( + + )} + + ); +}; diff --git a/yarn.lock b/yarn.lock index 072fb23cee..fbb16ac5c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"