Merge pull request #33389 from backstage/rugvip/nfs-header-page-migration

plugins: migrate NFS pages to HeaderPage
This commit is contained in:
Patrik Oldsberg
2026-03-17 22:16:12 +01:00
committed by GitHub
39 changed files with 1049 additions and 476 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-app': patch
'@backstage/plugin-app-react': patch
---
`NavContentBlueprint` nav item collections now keep previously collected `rest()` results in sync when additional items are taken later in the same render, making it easier to place items across multiple sidebar sections.
+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.
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/plugin-api-docs': patch
'@backstage/plugin-auth': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-unprocessed-entities': patch
'@backstage/plugin-devtools': patch
'@backstage/plugin-notifications': patch
'@backstage/plugin-search': 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.
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog-unprocessed-entities': patch
'@backstage/plugin-devtools': patch
'@backstage/plugin-devtools-react': minor
---
Removed the deprecated `DevToolsContentBlueprint` from `@backstage/plugin-devtools-react`. DevTools pages in the new frontend system now use `SubPageBlueprint` tabs instead, and the catalog unprocessed entities alpha extension now attaches to DevTools as a subpage.
-2
View File
@@ -146,8 +146,6 @@ app:
# - entity-content:azure-devops/pull-requests
# - entity-content:azure-devops/git-tags
# Enable the catalog-unprocessed-entities tab in devtools
- devtools-content:catalog-unprocessed-entities: true
# Disable the catalog-unprocessed-entities element outside devtools
- page:catalog-unprocessed-entities: false
+4 -3
View File
@@ -34,10 +34,11 @@ module.exports = {
/** Search */
'http://localhost:3000/search',
/** Miscellaneous */
'http://localhost:3000/settings',
// TODO(Rugvip): Figure out why these don't work after the BUI header switch
// 'http://localhost:3000/settings/general',
/** plugin-devtools */
'http://localhost:3000/devtools',
'http://localhost:3000/devtools/config',
// 'http://localhost:3000/devtools/info',
// 'http://localhost:3000/devtools/config',
/** plugin-explore */
'http://localhost:3000/explore',
'http://localhost:3000/explore/groups',
+6 -8
View File
@@ -27,15 +27,11 @@ import {
} from '@backstage/core-components';
import SearchIcon from '@material-ui/icons/Search';
import MenuIcon from '@material-ui/icons/Menu';
import BuildIcon from '@material-ui/icons/Build';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { NavContentBlueprint } from '@backstage/plugin-app-react';
import { SidebarSearchModal } from '@backstage/plugin-search';
import { NotificationsSidebarItem } from '@backstage/plugin-notifications';
import {
Settings,
UserSettingsSignInAvatar,
} from '@backstage/plugin-user-settings';
import { UserSettingsSignInAvatar } from '@backstage/plugin-user-settings';
import { makeStyles } from '@material-ui/core/styles';
const useSidebarLogoStyles = makeStyles({
@@ -111,7 +107,9 @@ export const appModuleNav = createFrontendModule({
text={item.title}
/>
));
nav.take('page:home'); // Skip home
// Skip these
nav.take('page:home');
nav.take('page:search');
return (
<Sidebar>
<SidebarLogo />
@@ -136,8 +134,8 @@ export const appModuleNav = createFrontendModule({
to="/settings"
>
<NotificationsSidebarItem />
<SidebarItem icon={BuildIcon} to="devtools" text="DevTools" />
<Settings />
{nav.take('page:devtools')}
{nav.take('page:user-settings')}
</SidebarGroup>
</Sidebar>
);
@@ -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 />);
+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,63 @@ 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={generatedSubtitle} customActions={headerActions} />
<Content>
<ApiExplorerPageContent
initiallySelectedFilter={initiallySelectedFilter}
columns={columns}
actions={actions}
ownerPickerMode={ownerPickerMode}
pagination={pagination}
/>
</Content>
</>
);
};
@@ -31,36 +31,72 @@ function mockNode(id: string): AppNode {
function mockNavItems(items: NavContentNavItem[]): NavContentNavItems {
const taken = new Set<string>();
let restItems: NavContentNavItem[] | undefined;
return {
take(id: string) {
const item = items.find(i => i.node.spec.id === id);
if (item) {
if (item && !taken.has(id)) {
taken.add(id);
if (restItems) {
const index = restItems.findIndex(
restItem => restItem.node.spec.id === id,
);
if (index !== -1) {
restItems.splice(index, 1);
}
}
}
return item;
},
rest: () => items.filter(i => !taken.has(i.node.spec.id)),
rest: () => {
if (!restItems) {
restItems = items.filter(i => !taken.has(i.node.spec.id));
}
return restItems;
},
clone() {
return mockNavItems(items);
},
withComponent(Component: (props: NavContentNavItem) => JSX.Element) {
let renderedItems: JSX.Element[] | undefined;
return {
take: (id: string) => {
const item = items.find(i => i.node.spec.id === id);
if (item) {
if (item && !taken.has(id)) {
taken.add(id);
return <Component {...item} />;
if (restItems) {
const index = restItems.findIndex(
restItem => restItem.node.spec.id === id,
);
if (index !== -1) {
restItems.splice(index, 1);
}
}
if (renderedItems) {
const index = renderedItems.findIndex(
renderedItem => renderedItem.key === item.node.spec.id,
);
if (index !== -1) {
renderedItems.splice(index, 1);
}
}
return <Component key={item.node.spec.id} {...item} />;
}
return null;
},
rest: (options?: { sortBy?: 'title' }) => {
const remaining = items.filter(i => !taken.has(i.node.spec.id));
if (options?.sortBy === 'title') {
remaining.sort((a, b) => a.title.localeCompare(b.title));
if (!restItems) {
restItems = items.filter(i => !taken.has(i.node.spec.id));
}
return remaining.map(item => (
<Component key={item.node.spec.id} {...item} />
));
if (!renderedItems) {
if (options?.sortBy === 'title') {
restItems.sort((a, b) => a.title.localeCompare(b.title));
}
renderedItems = restItems.map(item => (
<Component key={item.node.spec.id} {...item} />
));
}
return renderedItems;
},
};
},
@@ -267,4 +303,61 @@ describe('NavContentBlueprint', () => {
expect(docsLink).toBeInTheDocument();
expect(docsLink.closest('nav')).toBeTruthy();
});
it('should keep the rendered rest array in sync with later take calls', () => {
const items: NavContentNavItem[] = [
{
node: mockNode('page:catalog'),
href: '/catalog',
title: 'Catalog',
icon: <span>catalog</span>,
routeRef,
},
{
node: mockNode('page:devtools'),
href: '/devtools',
title: 'DevTools',
icon: <span>devtools</span>,
routeRef,
},
{
node: mockNode('page:user-settings'),
href: '/settings',
title: 'Settings',
icon: <span>settings</span>,
routeRef,
},
];
const extension = NavContentBlueprint.make({
name: 'test',
params: {
component: ({ navItems }) => {
const nav = navItems.withComponent(item => (
<a href={item.href}>{item.title}</a>
));
const rest = nav.rest({ sortBy: 'title' });
return (
<div>
<nav>{rest}</nav>
<aside>{nav.take('page:devtools')}</aside>
<footer>{nav.take('page:user-settings')}</footer>
</div>
);
},
},
});
const tester = createExtensionTester(extension);
const Component = tester.get(NavContentBlueprint.dataRefs.component);
render(<Component navItems={mockNavItems(items)} items={[]} />);
expect(screen.getByText('Catalog').closest('nav')).toBeTruthy();
expect(screen.queryByText('DevTools')?.closest('nav')).toBeFalsy();
expect(screen.queryByText('Settings')?.closest('nav')).toBeFalsy();
expect(screen.getByText('DevTools').closest('aside')).toBeTruthy();
expect(screen.getByText('Settings').closest('footer')).toBeTruthy();
});
});
@@ -53,7 +53,12 @@ export interface NavContentNavItem {
export interface NavContentNavItemsWithComponent {
/** Render and take a specific item by extension ID. Returns null if not found. */
take(id: string): JSX.Element | null;
/** Render all remaining items not yet taken, optionally sorted. */
/**
* Render all remaining items not yet taken, optionally sorted.
*
* The returned array is live and is updated when later `take` calls remove
* items from the collection.
*/
rest(options?: { sortBy?: 'title' }): JSX.Element[];
}
@@ -66,7 +71,12 @@ export interface NavContentNavItemsWithComponent {
export interface NavContentNavItems {
/** Take an item by extension ID, removing it from the collection. */
take(id: string): NavContentNavItem | undefined;
/** All items not yet taken. */
/**
* All items not yet taken.
*
* The returned array is live and is updated when later `take` calls remove
* items from the collection.
*/
rest(): NavContentNavItem[];
/** Create a copy of the collection preserving the current taken state. */
clone(): NavContentNavItems;
+35 -8
View File
@@ -41,6 +41,7 @@ class NavItemBag implements NavContentNavItems {
readonly #items: NavContentNavItem[];
readonly #index: Map<string, NavContentNavItem>;
readonly #taken: Set<string>;
#restItems: NavContentNavItem[] | undefined;
constructor(items: NavContentNavItem[], taken?: Iterable<string>) {
this.#items = items;
@@ -50,14 +51,27 @@ class NavItemBag implements NavContentNavItems {
take(id: string): NavContentNavItem | undefined {
const item = this.#index.get(id);
if (item) {
if (item && !this.#taken.has(id)) {
this.#taken.add(id);
if (this.#restItems) {
const index = this.#restItems.findIndex(
restItem => restItem.node.spec.id === id,
);
if (index !== -1) {
this.#restItems.splice(index, 1);
}
}
}
return item;
}
rest(): NavContentNavItem[] {
return this.#items.filter(item => !this.#taken.has(item.node.spec.id));
if (!this.#restItems) {
this.#restItems = this.#items.filter(
item => !this.#taken.has(item.node.spec.id),
);
}
return this.#restItems;
}
clone(): NavContentNavItems {
@@ -65,19 +79,32 @@ class NavItemBag implements NavContentNavItems {
}
withComponent(Component: (props: NavContentNavItem) => JSX.Element) {
let renderedItems: JSX.Element[] | undefined;
return {
take: (id: string) => {
const item = this.take(id);
return item ? <Component {...item} /> : null;
if (item && renderedItems) {
const index = renderedItems.findIndex(
renderedItem => renderedItem.key === item.node.spec.id,
);
if (index !== -1) {
renderedItems.splice(index, 1);
}
}
return item ? <Component key={item.node.spec.id} {...item} /> : null;
},
rest: (options?: { sortBy?: 'title' }) => {
const items = this.rest();
if (options?.sortBy === 'title') {
items.sort((a, b) => a.title.localeCompare(b.title));
if (!renderedItems) {
if (options?.sortBy === 'title') {
items.sort((a, b) => a.title.localeCompare(b.title));
}
renderedItems = items.map(item => (
<Component key={item.node.spec.id} {...item} />
));
}
return items.map(item => (
<Component key={item.node.spec.id} {...item} />
));
return renderedItems;
},
};
}
+22 -17
View File
@@ -29,6 +29,7 @@ import {
import { PluginHeader } from '@backstage/ui';
import Button from '@material-ui/core/Button';
import { useMemo } from 'react';
import { useResolvedPath } from 'react-router-dom';
export const Progress = SwappableComponentBlueprint.make({
name: 'core-progress',
@@ -75,31 +76,35 @@ export const PageLayout = SwappableComponentBlueprint.make({
component: SwappablePageLayout,
loader: () => (props: PageLayoutProps) => {
const { title, icon, noHeader, headerActions, tabs, children } = props;
const tabsWithMatchStrategy = useMemo(
// TODO(Rugvip): Different solution to this path handling would be good
const parentPath = useResolvedPath('.').pathname.replace(/\/$/, '');
const resolvedTabs = useMemo(
() =>
tabs?.map(tab => ({
...tab,
href: tab.href.startsWith('/')
? tab.href
: `${parentPath}/${tab.href}`.replace(/\/{2,}/g, '/'),
matchStrategy: 'prefix' as const,
})),
[tabs],
[tabs, parentPath],
);
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={resolvedTabs}
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"
@@ -56,7 +56,7 @@
"@backstage/errors": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-catalog-unprocessed-entities-common": "workspace:^",
"@backstage/plugin-devtools-react": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.60",
@@ -7,7 +7,6 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api';
import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
import { ApiFactory } from '@backstage/frontend-plugin-api';
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
import { DevToolsContentBlueprintParams } from '@backstage/plugin-devtools-react';
import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api';
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
import { ExtensionInput } from '@backstage/frontend-plugin-api';
@@ -144,8 +143,8 @@ export default _default;
// @alpha
export const unprocessedEntitiesDevToolsContent: OverridableExtensionDefinition<{
kind: 'devtools-content';
name: undefined;
kind: 'sub-page';
name: 'unprocessed-entities';
config: {
path: string | undefined;
title: string | undefined;
@@ -164,9 +163,22 @@ export const unprocessedEntitiesDevToolsContent: OverridableExtensionDefinition<
}
>
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.title', {}>;
| ExtensionDataRef<string, 'core.title', {}>
| ExtensionDataRef<
IconElement,
'core.icon',
{
optional: true;
}
>;
inputs: {};
params: DevToolsContentBlueprintParams;
params: {
path: string;
title: string;
icon?: IconElement;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef_2;
};
}>;
// (No @packageDocumentation comment for this package)
@@ -14,23 +14,25 @@
* limitations under the License.
*/
import { DevToolsContentBlueprint } from '@backstage/plugin-devtools-react';
import { SubPageBlueprint } from '@backstage/frontend-plugin-api';
import { Content } from '@backstage/core-components';
/**
* DevTools content for catalog unprocessed entities.
*
* @alpha
*/
export const unprocessedEntitiesDevToolsContent = DevToolsContentBlueprint.make(
{
disabled: true,
params: {
path: 'unprocessed-entities',
title: 'Unprocessed Entities',
loader: () =>
import('../components/UnprocessedEntities').then(m => (
export const unprocessedEntitiesDevToolsContent = SubPageBlueprint.make({
attachTo: { id: 'page:devtools', input: 'pages' },
name: 'unprocessed-entities',
params: {
path: 'unprocessed-entities',
title: 'Unprocessed Entities',
loader: () =>
import('../components/UnprocessedEntities').then(m => (
<Content>
<m.UnprocessedEntitiesContent />
)),
},
</Content>
)),
},
);
});
@@ -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>
</>
);
};
+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}
/>
);
}
-44
View File
@@ -3,48 +3,4 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react';
import { RouteRef } from '@backstage/frontend-plugin-api';
// @public
export const DevToolsContentBlueprint: ExtensionBlueprint<{
kind: 'devtools-content';
params: DevToolsContentBlueprintParams;
output:
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.title', {}>;
inputs: {};
config: {
path: string | undefined;
title: string | undefined;
};
configInput: {
title?: string | undefined;
path?: string | undefined;
};
dataRefs: never;
}>;
// @public
export interface DevToolsContentBlueprintParams {
// (undocumented)
loader: () => Promise<JSX_2.Element>;
// (undocumented)
path: string;
// (undocumented)
routeRef?: RouteRef;
// (undocumented)
title: string;
}
```
@@ -1,87 +0,0 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
coreExtensionData,
createExtensionBlueprint,
ExtensionBoundary,
RouteRef,
} from '@backstage/frontend-plugin-api';
import { JSX } from 'react';
/**
* Parameters for creating a DevTools route extension
* @public
*/
export interface DevToolsContentBlueprintParams {
path: string;
title: string;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef;
}
/**
* Extension blueprint for creating DevTools content pages (appearing as tabs)
*
* @example
* ```tsx
* const myDevToolsContent = DevToolsContentBlueprint.make({
* {
* params: {
* path: 'my-dev-tools',
* title: 'My DevTools',
* loader: () =>
* import('../components/MyDevTools').then(m =>
* compatWrapper(<m.MyDevTools />),
* ),
* },
* },
* });
* ```
* @public
*/
export const DevToolsContentBlueprint = createExtensionBlueprint({
kind: 'devtools-content',
attachTo: { id: 'page:devtools', input: 'contents' },
output: [
coreExtensionData.reactElement,
coreExtensionData.routePath,
coreExtensionData.routeRef.optional(),
coreExtensionData.title,
],
config: {
schema: {
path: z => z.string().optional(),
title: z => z.string().optional(),
},
},
*factory(params: DevToolsContentBlueprintParams, { node, config }) {
const path = config.path ?? params.path;
const title = config.title ?? params.title;
yield coreExtensionData.reactElement(
ExtensionBoundary.lazy(node, params.loader),
);
yield coreExtensionData.routePath(path);
yield coreExtensionData.title(title);
if (params.routeRef) {
yield coreExtensionData.routeRef(params.routeRef);
}
},
});
+2 -4
View File
@@ -19,7 +19,5 @@
*
* @packageDocumentation
*/
export {
type DevToolsContentBlueprintParams,
DevToolsContentBlueprint,
} from './devToolsContentBlueprint';
export {};
+1 -1
View File
@@ -59,8 +59,8 @@
"@backstage/errors": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-devtools-common": "workspace:^",
"@backstage/plugin-devtools-react": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.57",
+114 -17
View File
@@ -125,23 +125,6 @@ const _default: OverridableFrontendPlugin<
internal: false;
}
>;
contents: ExtensionInput<
| ConfigurableExtensionDataRef<string, 'core.title', {}>
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef_2<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>,
{
singleton: false;
optional: true;
internal: false;
}
>;
};
kind: 'page';
name: undefined;
@@ -154,6 +137,120 @@ const _default: OverridableFrontendPlugin<
noHeader?: boolean;
};
}>;
'sub-page:devtools/config': OverridableExtensionDefinition<{
kind: 'sub-page';
name: 'config';
config: {
path: string | undefined;
title: string | undefined;
};
configInput: {
title?: string | undefined;
path?: string | undefined;
};
output:
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<
RouteRef_2<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.title', {}>
| ExtensionDataRef<
IconElement,
'core.icon',
{
optional: true;
}
>;
inputs: {};
params: {
path: string;
title: string;
icon?: IconElement;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef_2;
};
}>;
'sub-page:devtools/info': OverridableExtensionDefinition<{
kind: 'sub-page';
name: 'info';
config: {
path: string | undefined;
title: string | undefined;
};
configInput: {
title?: string | undefined;
path?: string | undefined;
};
output:
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<
RouteRef_2<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.title', {}>
| ExtensionDataRef<
IconElement,
'core.icon',
{
optional: true;
}
>;
inputs: {};
params: {
path: string;
title: string;
icon?: IconElement;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef_2;
};
}>;
'sub-page:devtools/scheduled-tasks': OverridableExtensionDefinition<{
kind: 'sub-page';
name: 'scheduled-tasks';
config: {
path: string | undefined;
title: string | undefined;
};
configInput: {
title?: string | undefined;
path?: string | undefined;
};
output:
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<
RouteRef_2<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.title', {}>
| ExtensionDataRef<
IconElement,
'core.icon',
{
optional: true;
}
>;
inputs: {};
params: {
path: string;
title: string;
icon?: IconElement;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef_2;
};
}>;
}
>;
export default _default;
+93 -29
View File
@@ -16,18 +16,25 @@
import {
createFrontendPlugin,
coreExtensionData,
discoveryApiRef,
fetchApiRef,
ApiBlueprint,
PageBlueprint,
NavItemBlueprint,
createExtensionInput,
coreExtensionData,
SubPageBlueprint,
} from '@backstage/frontend-plugin-api';
import { devToolsApiRef, DevToolsClient } from '../api';
import BuildIcon from '@material-ui/icons/Build';
import { Content } from '@backstage/core-components';
import { rootRouteRef } from '../routes';
import {
devToolsConfigReadPermission,
devToolsInfoReadPermission,
} from '@backstage/plugin-devtools-common';
import { devToolsTaskSchedulerReadPermission } from '@backstage/plugin-devtools-common/alpha';
import { RequirePermission } from '@backstage/plugin-permission-react';
/** @alpha */
export const devToolsApi = ApiBlueprint.make({
@@ -45,34 +52,84 @@ export const devToolsApi = ApiBlueprint.make({
/** @alpha */
export const devToolsPage = PageBlueprint.makeWithOverrides({
inputs: {
contents: createExtensionInput(
[
coreExtensionData.reactElement,
coreExtensionData.routePath,
coreExtensionData.routeRef.optional(),
coreExtensionData.title,
],
{
optional: true,
},
),
},
factory(originalFactory, { inputs }) {
return originalFactory({
path: '/devtools',
routeRef: rootRouteRef,
loader: () => {
const contents = inputs.contents.map(content => ({
path: content.get(coreExtensionData.routePath),
title: content.get(coreExtensionData.title),
children: content.get(coreExtensionData.reactElement),
}));
return import('../components/DevToolsPage').then(m => (
<m.DevToolsPage contents={contents} />
));
},
const pages = [...inputs.pages].sort((left, right) => {
const leftPath = left.get(coreExtensionData.routePath);
const rightPath = right.get(coreExtensionData.routePath);
if (leftPath === 'info' && rightPath !== 'info') {
return -1;
}
if (leftPath !== 'info' && rightPath === 'info') {
return 1;
}
return 0;
});
return originalFactory(
{
path: '/devtools',
routeRef: rootRouteRef,
title: 'DevTools',
},
{
inputs: {
pages,
},
},
);
},
});
/** @alpha */
export const devToolsInfoPage = SubPageBlueprint.make({
name: 'info',
params: {
path: 'info',
title: 'Info',
loader: () =>
import('../components/Content').then(m => (
<Content>
<RequirePermission permission={devToolsInfoReadPermission}>
<m.InfoContent />
</RequirePermission>
</Content>
)),
},
});
/** @alpha */
export const devToolsConfigPage = SubPageBlueprint.make({
name: 'config',
params: {
path: 'config',
title: 'Config',
loader: () =>
import('../components/Content').then(m => (
<Content>
<RequirePermission permission={devToolsConfigReadPermission}>
<m.ConfigContent />
</RequirePermission>
</Content>
)),
},
});
/** @alpha */
export const devToolsScheduledTasksPage = SubPageBlueprint.make({
name: 'scheduled-tasks',
params: {
path: 'scheduled-tasks',
title: 'Scheduled Tasks',
loader: () =>
import('../components/Content').then(m => (
<Content>
<RequirePermission permission={devToolsTaskSchedulerReadPermission}>
<m.ScheduledTasksContent />
</RequirePermission>
</Content>
)),
},
});
@@ -94,5 +151,12 @@ export default createFrontendPlugin({
routes: {
root: rootRouteRef,
},
extensions: [devToolsApi, devToolsPage, devToolsNavItem],
extensions: [
devToolsApi,
devToolsPage,
devToolsInfoPage,
devToolsConfigPage,
devToolsScheduledTasksPage,
devToolsNavItem,
],
});
@@ -21,7 +21,7 @@ import {
import { ConfigContent } from '../Content';
import { devToolsTaskSchedulerReadPermission } from '@backstage/plugin-devtools-common/alpha';
import { DevToolsLayout } from '../DevToolsLayout';
import { DevToolsLayout } from '../DevToolsLayout/DevToolsLayout';
import { InfoContent } from '../Content';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { ScheduledTasksContent } from '../Content/ScheduledTasksContent';
+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 />
)),
},
});
@@ -66,7 +66,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 +78,8 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
type,
typeLink,
markAsReadOnLinkOpen,
} = props ?? {};
headerVariant,
} = props;
const [refresh, setRefresh] = useState(false);
const { lastSignal } = useSignal('notifications');
@@ -186,6 +189,52 @@ 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 pageContent;
}
return (
<PageWithHeader
title={title}
@@ -195,45 +244,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
@@ -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",
+98 -103
View File
@@ -23,8 +23,6 @@ import {
CatalogIcon,
Content,
DocsIcon,
Header,
Page,
useSidebarPinState,
} from '@backstage/core-components';
import {
@@ -143,113 +141,110 @@ export const searchPage = PageBlueprint.makeWithOverrides({
const configApi = useApi(configApiRef);
return (
<Page themeId="home">
{!isMobile && <Header title="Search" />}
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<SearchBar debounceTime={100} />
</Grid>
{!isMobile && (
<Grid item xs={3}>
<SearchType.Accordion
name="Result Type"
defaultValue={configApi.getOptionalString(
'search.defaultType',
)}
showCounts
types={[
{
value: 'software-catalog',
name: 'Software Catalog',
icon: <CatalogIcon />,
},
{
value: 'techdocs',
name: 'Documentation',
icon: <DocsIcon />,
},
].concat(resultTypes)}
/>
<Paper className={classes.filters}>
{types.includes('techdocs') && (
<SearchFilter.Select
className={classes.filter}
label="Entity"
name="name"
values={async () => {
// Return a list of entities which are documented.
const { items } = await catalogApi.getEntities({
fields: ['metadata.name'],
filter: {
'metadata.annotations.backstage.io/techdocs-ref':
CATALOG_FILTER_EXISTS,
},
});
const names = items.map(
entity => entity.metadata.name,
);
names.sort();
return names;
}}
/>
)}
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<SearchBar debounceTime={100} />
</Grid>
{!isMobile && (
<Grid item xs={3}>
<SearchType.Accordion
name="Result Type"
defaultValue={configApi.getOptionalString(
'search.defaultType',
)}
showCounts
types={[
{
value: 'software-catalog',
name: 'Software Catalog',
icon: <CatalogIcon />,
},
{
value: 'techdocs',
name: 'Documentation',
icon: <DocsIcon />,
},
].concat(resultTypes)}
/>
<Paper className={classes.filters}>
{types.includes('techdocs') && (
<SearchFilter.Select
className={classes.filter}
label="Kind"
name="kind"
values={[
'API',
'Component',
'Domain',
'Group',
'Location',
'Resource',
'System',
'Template',
'User',
]}
/>
<SearchFilter.Checkbox
className={classes.filter}
label="Lifecycle"
name="lifecycle"
values={['experimental', 'production']}
/>
{additionalSearchFilters.map(SearchFilterComponent => (
<SearchFilterComponent className={classes.filter} />
))}
</Paper>
</Grid>
)}
<Grid item xs>
<SearchPagination />
<SearchResults>
{({ results }) => (
<>
{results.map((result, index) => {
const { noTrack } = config;
const { document, ...rest } = result;
const SearchResultListItem =
getResultItemComponent(result);
return (
<SearchResultListItem
{...rest}
key={index}
result={document}
noTrack={noTrack}
/>
label="Entity"
name="name"
values={async () => {
// Return a list of entities which are documented.
const { items } = await catalogApi.getEntities({
fields: ['metadata.name'],
filter: {
'metadata.annotations.backstage.io/techdocs-ref':
CATALOG_FILTER_EXISTS,
},
});
const names = items.map(
entity => entity.metadata.name,
);
})}
</>
names.sort();
return names;
}}
/>
)}
</SearchResults>
<SearchResultPager />
<SearchFilter.Select
className={classes.filter}
label="Kind"
name="kind"
values={[
'API',
'Component',
'Domain',
'Group',
'Location',
'Resource',
'System',
'Template',
'User',
]}
/>
<SearchFilter.Checkbox
className={classes.filter}
label="Lifecycle"
name="lifecycle"
values={['experimental', 'production']}
/>
{additionalSearchFilters.map(SearchFilterComponent => (
<SearchFilterComponent className={classes.filter} />
))}
</Paper>
</Grid>
)}
<Grid item xs>
<SearchPagination />
<SearchResults>
{({ results }) => (
<>
{results.map((result, index) => {
const { noTrack } = config;
const { document, ...rest } = result;
const SearchResultListItem =
getResultItemComponent(result);
return (
<SearchResultListItem
{...rest}
key={index}
result={document}
noTrack={noTrack}
/>
);
})}
</>
)}
</SearchResults>
<SearchResultPager />
</Grid>
</Content>
</Page>
</Grid>
</Content>
);
};
+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",
+120 -5
View File
@@ -45,6 +45,8 @@ const _default: OverridableFrontendPlugin<
};
}>;
'page:user-settings': OverridableExtensionDefinition<{
kind: 'page';
name: undefined;
config: {
path: string | undefined;
title: string | undefined;
@@ -108,6 +110,44 @@ const _default: OverridableFrontendPlugin<
internal: false;
}
>;
};
params: {
path: string;
title?: string;
icon?: IconElement;
loader?: () => Promise<JSX_2.Element>;
routeRef?: RouteRef;
noHeader?: boolean;
};
}>;
'sub-page:user-settings/auth-providers': OverridableExtensionDefinition<{
config: {
path: string | undefined;
title: string | undefined;
};
configInput: {
title?: string | undefined;
path?: string | undefined;
};
output:
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.title', {}>
| ExtensionDataRef<
IconElement,
'core.icon',
{
optional: true;
}
>;
inputs: {
providerSettings: ExtensionInput<
ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>,
{
@@ -117,15 +157,90 @@ const _default: OverridableFrontendPlugin<
}
>;
};
kind: 'page';
name: undefined;
kind: 'sub-page';
name: 'auth-providers';
params: {
path: string;
title?: string;
title: string;
icon?: IconElement;
loader?: () => Promise<JSX_2.Element>;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef;
};
}>;
'sub-page:user-settings/feature-flags': OverridableExtensionDefinition<{
kind: 'sub-page';
name: 'feature-flags';
config: {
path: string | undefined;
title: string | undefined;
};
configInput: {
title?: string | undefined;
path?: string | undefined;
};
output:
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.title', {}>
| ExtensionDataRef<
IconElement,
'core.icon',
{
optional: true;
}
>;
inputs: {};
params: {
path: string;
title: string;
icon?: IconElement;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef;
};
}>;
'sub-page:user-settings/general': OverridableExtensionDefinition<{
kind: 'sub-page';
name: 'general';
config: {
path: string | undefined;
title: string | undefined;
};
configInput: {
title?: string | undefined;
path?: string | undefined;
};
output:
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.title', {}>
| ExtensionDataRef<
IconElement,
'core.icon',
{
optional: true;
}
>;
inputs: {};
params: {
path: string;
title: string;
icon?: IconElement;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef;
noHeader?: boolean;
};
}>;
}
+57 -10
View File
@@ -19,7 +19,9 @@ import {
createFrontendPlugin,
PageBlueprint,
NavItemBlueprint,
SubPageBlueprint,
} from '@backstage/frontend-plugin-api';
import { Content } from '@backstage/core-components';
import SettingsIcon from '@material-ui/icons/Settings';
import { settingsRouteRef } from './plugin';
@@ -31,7 +33,30 @@ import { userSettingsTranslationRef as _userSettingsTranslationRef } from './tra
*/
export const userSettingsTranslationRef = _userSettingsTranslationRef;
const userSettingsPage = PageBlueprint.makeWithOverrides({
const userSettingsPage = PageBlueprint.make({
params: {
path: '/settings',
routeRef: settingsRouteRef,
title: 'Settings',
},
});
const generalSettingsPage = SubPageBlueprint.make({
name: 'general',
params: {
path: 'general',
title: 'General',
loader: () =>
import('./components/General').then(m => (
<Content>
<m.UserSettingsGeneral />
</Content>
)),
},
});
const authProvidersSettingsPage = SubPageBlueprint.makeWithOverrides({
name: 'auth-providers',
inputs: {
providerSettings: createExtensionInput([coreExtensionData.reactElement], {
singleton: true,
@@ -40,20 +65,36 @@ const userSettingsPage = PageBlueprint.makeWithOverrides({
},
factory(originalFactory, { inputs }) {
return originalFactory({
path: '/settings',
routeRef: settingsRouteRef,
path: 'auth-providers',
title: 'Authentication Providers',
loader: () =>
import('./components/SettingsPage').then(m => (
<m.SettingsPage
providerSettings={inputs.providerSettings?.get(
coreExtensionData.reactElement,
)}
/>
import('./components/AuthProviders').then(m => (
<Content>
<m.UserSettingsAuthProviders
providerSettings={inputs.providerSettings?.get(
coreExtensionData.reactElement,
)}
/>
</Content>
)),
});
},
});
const featureFlagsSettingsPage = SubPageBlueprint.make({
name: 'feature-flags',
params: {
path: 'feature-flags',
title: 'Feature Flags',
loader: () =>
import('./components/FeatureFlags').then(m => (
<Content>
<m.UserSettingsFeatureFlags />
</Content>
)),
},
});
/** @alpha */
export const settingsNavItem = NavItemBlueprint.make({
params: {
@@ -71,7 +112,13 @@ export default createFrontendPlugin({
title: 'Settings',
icon: <SettingsIcon fontSize="inherit" />,
info: { packageJson: () => import('../package.json') },
extensions: [userSettingsPage, settingsNavItem],
extensions: [
userSettingsPage,
generalSettingsPage,
authProvidersSettingsPage,
featureFlagsSettingsPage,
settingsNavItem,
],
routes: {
root: settingsRouteRef,
},
@@ -18,7 +18,8 @@ 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 { SettingsLayout } from '../SettingsLayout/SettingsLayout';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { userSettingsTranslationRef } from '../../translation';
+7 -3
View File
@@ -4679,6 +4679,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"
@@ -5383,7 +5384,7 @@ __metadata:
"@backstage/errors": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-catalog-unprocessed-entities-common": "workspace:^"
"@backstage/plugin-devtools-react": "workspace:^"
"@backstage/ui": "workspace:^"
"@material-ui/core": "npm:^4.9.13"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:^4.0.0-alpha.60"
@@ -5546,7 +5547,7 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-devtools-react@workspace:^, @backstage/plugin-devtools-react@workspace:plugins/devtools-react":
"@backstage/plugin-devtools-react@workspace:plugins/devtools-react":
version: 0.0.0-use.local
resolution: "@backstage/plugin-devtools-react@workspace:plugins/devtools-react"
dependencies:
@@ -5584,8 +5585,8 @@ __metadata:
"@backstage/errors": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-devtools-common": "workspace:^"
"@backstage/plugin-devtools-react": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"
"@backstage/ui": "workspace:^"
"@material-ui/core": "npm:^4.9.13"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:^4.0.0-alpha.57"
@@ -6273,6 +6274,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"
@@ -7311,6 +7313,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"
@@ -7737,6 +7740,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"