Merge pull request #33389 from backstage/rugvip/nfs-header-page-migration
plugins: migrate NFS pages to HeaderPage
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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 />);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -19,7 +19,5 @@
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export {
|
||||
type DevToolsContentBlueprintParams,
|
||||
DevToolsContentBlueprint,
|
||||
} from './devToolsContentBlueprint';
|
||||
|
||||
export {};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
);
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user