plugin icons and titles
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import { ExpandRecursive } from '@backstage/types';
|
||||
import { ExtensionBlueprint as ExtensionBlueprint_2 } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionBlueprintParams as ExtensionBlueprintParams_2 } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDataRef as ExtensionDataRef_2 } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionInput as ExtensionInput_2 } from '@backstage/frontend-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||
@@ -388,6 +389,7 @@ export interface ConfigurableExtensionDataRef<
|
||||
// @public (undocumented)
|
||||
export const coreExtensionData: {
|
||||
title: ConfigurableExtensionDataRef_2<string, 'core.title', {}>;
|
||||
icon: ConfigurableExtensionDataRef_2<IconComponent, 'core.icon', {}>;
|
||||
reactElement: ConfigurableExtensionDataRef_2<
|
||||
JSX_3.Element,
|
||||
'core.reactElement',
|
||||
@@ -1367,12 +1369,14 @@ export interface FrontendPlugin<
|
||||
readonly $$type: '@backstage/FrontendPlugin';
|
||||
// (undocumented)
|
||||
readonly externalRoutes: TExternalRoutes;
|
||||
readonly icon?: IconComponent;
|
||||
// @deprecated
|
||||
readonly id: string;
|
||||
info(): Promise<FrontendPluginInfo>;
|
||||
readonly pluginId: string;
|
||||
// (undocumented)
|
||||
readonly routes: TRoutes;
|
||||
readonly title?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -1420,6 +1424,19 @@ export const googleAuthApiRef: ApiRef<
|
||||
SessionApi
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const HeaderActionBlueprint: ExtensionBlueprint_2<{
|
||||
kind: 'header-action';
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
};
|
||||
output: ExtensionDataRef_2<JSX_3, 'core.reactElement', {}>;
|
||||
inputs: {};
|
||||
config: {};
|
||||
configInput: {};
|
||||
dataRefs: never;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export type IconComponent = ComponentType<{
|
||||
fontSize?: 'medium' | 'large' | 'small' | 'inherit';
|
||||
@@ -1710,29 +1727,101 @@ export const PageBlueprint: ExtensionBlueprint_2<{
|
||||
params: {
|
||||
defaultPath?: [Error: `Use the 'path' param instead`];
|
||||
path: string;
|
||||
loader: () => Promise<JSX.Element>;
|
||||
title?: string;
|
||||
icon?: IconComponent;
|
||||
loader?: () => Promise<JSX.Element>;
|
||||
routeRef?: RouteRef;
|
||||
};
|
||||
output:
|
||||
| ExtensionDataRef_2<string, 'core.routing.path', {}>
|
||||
| ExtensionDataRef_2<JSX_3, 'core.reactElement', {}>
|
||||
| ExtensionDataRef_2<
|
||||
RouteRef<AnyRouteRefParams_2>,
|
||||
'core.routing.ref',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>
|
||||
| ExtensionDataRef_2<JSX_3, 'core.reactElement', {}>
|
||||
| ExtensionDataRef_2<
|
||||
string,
|
||||
'core.title',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>
|
||||
| ExtensionDataRef_2<
|
||||
IconComponent,
|
||||
'core.icon',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>;
|
||||
inputs: {};
|
||||
inputs: {
|
||||
pages: ExtensionInput_2<
|
||||
| ConfigurableExtensionDataRef_2<JSX_3, 'core.reactElement', {}>
|
||||
| ConfigurableExtensionDataRef_2<string, 'core.routing.path', {}>
|
||||
| ConfigurableExtensionDataRef_2<
|
||||
RouteRef<AnyRouteRefParams_2>,
|
||||
'core.routing.ref',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef_2<
|
||||
string,
|
||||
'core.title',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>,
|
||||
{
|
||||
singleton: false;
|
||||
optional: false;
|
||||
internal: false;
|
||||
}
|
||||
>;
|
||||
};
|
||||
config: {
|
||||
path: string | undefined;
|
||||
title: string | undefined;
|
||||
};
|
||||
configInput: {
|
||||
title?: string | undefined;
|
||||
path?: string | undefined;
|
||||
};
|
||||
dataRefs: never;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export const PageLayout: {
|
||||
(props: PageLayoutProps): JSX.Element | null;
|
||||
ref: SwappableComponentRef_2<PageLayoutProps, PageLayoutProps>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface PageLayoutProps {
|
||||
// (undocumented)
|
||||
children?: ReactNode;
|
||||
// (undocumented)
|
||||
icon?: IconComponent;
|
||||
// (undocumented)
|
||||
tabs?: PageTab[];
|
||||
// (undocumented)
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface PageTab {
|
||||
// (undocumented)
|
||||
href: string;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
label: string;
|
||||
// (undocumented)
|
||||
matchStrategy?: 'prefix' | 'exact';
|
||||
}
|
||||
|
||||
// @public
|
||||
export type PendingOAuthRequest = {
|
||||
provider: AuthProviderInfo;
|
||||
@@ -1757,12 +1846,14 @@ export interface PluginOptions<
|
||||
externalRoutes?: TExternalRoutes;
|
||||
// (undocumented)
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
icon?: IconComponent;
|
||||
// (undocumented)
|
||||
info?: FrontendPluginInfoOptions;
|
||||
// (undocumented)
|
||||
pluginId: TId;
|
||||
// (undocumented)
|
||||
routes?: TRoutes;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -1898,6 +1989,38 @@ export type StorageValueSnapshot<TValue extends JsonValue> =
|
||||
value: TValue;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const SubPageBlueprint: ExtensionBlueprint_2<{
|
||||
kind: 'sub-page';
|
||||
params: {
|
||||
path: string;
|
||||
title: string;
|
||||
loader: () => Promise<JSX.Element>;
|
||||
routeRef?: RouteRef;
|
||||
};
|
||||
output:
|
||||
| ExtensionDataRef_2<string, 'core.routing.path', {}>
|
||||
| ExtensionDataRef_2<
|
||||
RouteRef<AnyRouteRefParams_2>,
|
||||
'core.routing.ref',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>
|
||||
| ExtensionDataRef_2<JSX_3, 'core.reactElement', {}>
|
||||
| ExtensionDataRef_2<string, 'core.title', {}>;
|
||||
inputs: {};
|
||||
config: {
|
||||
path: string | undefined;
|
||||
title: string | undefined;
|
||||
};
|
||||
configInput: {
|
||||
title?: string | undefined;
|
||||
path?: string | undefined;
|
||||
};
|
||||
dataRefs: never;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export interface SubRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { IconComponent } from '../icons/types';
|
||||
import { RouteRef } from '../routing';
|
||||
import {
|
||||
coreExtensionData,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
import { ExtensionBoundary, PageLayout, PageTab } from '../components';
|
||||
|
||||
/**
|
||||
* Createx extensions that are routable React page components.
|
||||
* Creates extensions that are routable React page components.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -44,6 +45,7 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
coreExtensionData.reactElement,
|
||||
coreExtensionData.routeRef.optional(),
|
||||
coreExtensionData.title.optional(),
|
||||
coreExtensionData.icon.optional(),
|
||||
],
|
||||
config: {
|
||||
schema: {
|
||||
@@ -54,30 +56,35 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
*factory(
|
||||
params: {
|
||||
/**
|
||||
* @deprecated Use the `path' param instead.
|
||||
* @deprecated Use the `path` param instead.
|
||||
*/
|
||||
defaultPath?: [Error: `Use the 'path' param instead`];
|
||||
path: string;
|
||||
title?: string;
|
||||
icon?: IconComponent;
|
||||
loader?: () => Promise<JSX.Element>;
|
||||
routeRef?: RouteRef;
|
||||
},
|
||||
{ config, node, inputs },
|
||||
) {
|
||||
const title = config.title ?? params.title ?? node.spec.plugin.pluginId;
|
||||
const title =
|
||||
config.title ??
|
||||
params.title ??
|
||||
node.spec.plugin.title ??
|
||||
node.spec.plugin.pluginId;
|
||||
const icon = params.icon ?? node.spec.plugin.icon;
|
||||
|
||||
yield coreExtensionData.routePath(config.path ?? params.path);
|
||||
if (params.loader) {
|
||||
// Simple page with loader - render header + content
|
||||
const loader = params.loader; // Capture for closure
|
||||
const loader = params.loader;
|
||||
const PageContent = () => (
|
||||
<PageLayout title={title}>
|
||||
<PageLayout title={title} icon={icon}>
|
||||
{ExtensionBoundary.lazy(node, loader)}
|
||||
</PageLayout>
|
||||
);
|
||||
yield coreExtensionData.reactElement(<PageContent />);
|
||||
} else if (inputs.pages.length > 0) {
|
||||
// Parent page with sub-pages - render Header with tabs and Routes for sub-pages
|
||||
// Parent page with sub-pages - render header with tabs
|
||||
const tabs: PageTab[] = inputs.pages.map(page => {
|
||||
const path = page.get(coreExtensionData.routePath);
|
||||
const tabTitle = page.get(coreExtensionData.title);
|
||||
@@ -90,13 +97,11 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
});
|
||||
|
||||
const PageContent = () => {
|
||||
// Get first sub-page path for default navigation
|
||||
const firstPagePath = inputs.pages[0]?.get(coreExtensionData.routePath);
|
||||
|
||||
return (
|
||||
<PageLayout title={title} tabs={tabs}>
|
||||
<PageLayout title={title} icon={icon} tabs={tabs}>
|
||||
<Routes>
|
||||
{/* Index route redirects to first sub-page */}
|
||||
{firstPagePath && (
|
||||
<Route
|
||||
index
|
||||
@@ -117,8 +122,9 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
|
||||
yield coreExtensionData.reactElement(<PageContent />);
|
||||
} else {
|
||||
// Parent page without loader or sub-pages - render just header
|
||||
yield coreExtensionData.reactElement(<PageLayout title={title} />);
|
||||
yield coreExtensionData.reactElement(
|
||||
<PageLayout title={title} icon={icon} />,
|
||||
);
|
||||
}
|
||||
if (params.routeRef) {
|
||||
yield coreExtensionData.routeRef(params.routeRef);
|
||||
@@ -126,5 +132,8 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
if (title) {
|
||||
yield coreExtensionData.title(title);
|
||||
}
|
||||
if (icon) {
|
||||
yield coreExtensionData.icon(icon);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { IconComponent } from '../icons/types';
|
||||
import { createSwappableComponent } from './createSwappableComponent';
|
||||
|
||||
/**
|
||||
@@ -34,6 +35,7 @@ export interface PageTab {
|
||||
*/
|
||||
export interface PageLayoutProps {
|
||||
title?: string;
|
||||
icon?: IconComponent;
|
||||
tabs?: PageTab[];
|
||||
children?: ReactNode;
|
||||
}
|
||||
@@ -42,7 +44,7 @@ export interface PageLayoutProps {
|
||||
* Default implementation of PageLayout using plain HTML elements
|
||||
*/
|
||||
function DefaultPageLayout(props: PageLayoutProps): JSX.Element {
|
||||
const { title, tabs, children } = props;
|
||||
const { title, icon: Icon, tabs, children } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -65,11 +67,15 @@ function DefaultPageLayout(props: PageLayoutProps): JSX.Element {
|
||||
{title && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '12px 24px 8px',
|
||||
fontSize: '18px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{Icon && <Icon fontSize="small" />}
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
@@ -116,7 +122,7 @@ function DefaultPageLayout(props: PageLayoutProps): JSX.Element {
|
||||
/**
|
||||
* Swappable component for laying out page content with header and navigation.
|
||||
* The default implementation uses plain HTML elements.
|
||||
* Apps can override this with a custom implementation (e.g., using @backstage/ui).
|
||||
* Apps can override this with a custom implementation (e.g., using \@backstage/ui).
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
*/
|
||||
|
||||
import { JSX } from 'react';
|
||||
import { IconComponent } from '../icons/types';
|
||||
import { RouteRef } from '../routing/RouteRef';
|
||||
import { createExtensionDataRef } from './createExtensionDataRef';
|
||||
|
||||
/** @public */
|
||||
export const coreExtensionData = {
|
||||
title: createExtensionDataRef<string>().with({ id: 'core.title' }),
|
||||
icon: createExtensionDataRef<IconComponent>().with({ id: 'core.icon' }),
|
||||
reactElement: createExtensionDataRef<JSX.Element>().with({
|
||||
id: 'core.reactElement',
|
||||
}),
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { FeatureFlagConfig } from './types';
|
||||
import { MakeSortedExtensionsMap } from './MakeSortedExtensionsMap';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { IconComponent } from '../icons/types';
|
||||
import { RouteRef, SubRouteRef, ExternalRouteRef } from '../routing';
|
||||
import { ID_PATTERN } from './constants';
|
||||
|
||||
@@ -141,6 +142,15 @@ export interface FrontendPlugin<
|
||||
* @deprecated Use `pluginId` instead.
|
||||
*/
|
||||
readonly id: string;
|
||||
/**
|
||||
* The display title of the plugin, used in page headers and navigation.
|
||||
* Falls back to the plugin ID if not provided.
|
||||
*/
|
||||
readonly title?: string;
|
||||
/**
|
||||
* The display icon of the plugin, used in page headers and navigation.
|
||||
*/
|
||||
readonly icon?: IconComponent;
|
||||
readonly routes: TRoutes;
|
||||
readonly externalRoutes: TExternalRoutes;
|
||||
|
||||
@@ -158,6 +168,15 @@ export interface PluginOptions<
|
||||
TExtensions extends readonly ExtensionDefinition[],
|
||||
> {
|
||||
pluginId: TId;
|
||||
/**
|
||||
* The display title of the plugin, used in page headers and navigation.
|
||||
* Falls back to the plugin ID if not provided.
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* The display icon of the plugin, used in page headers and navigation.
|
||||
*/
|
||||
icon?: IconComponent;
|
||||
routes?: TRoutes;
|
||||
externalRoutes?: TExternalRoutes;
|
||||
extensions?: TExtensions;
|
||||
@@ -250,6 +269,8 @@ export function createFrontendPlugin<
|
||||
return OpaqueFrontendPlugin.createInstance('v1', {
|
||||
pluginId,
|
||||
id: pluginId,
|
||||
title: options.title,
|
||||
icon: options.icon,
|
||||
routes: options.routes ?? ({} as TRoutes),
|
||||
externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes),
|
||||
featureFlags: options.featureFlags ?? [],
|
||||
|
||||
Reference in New Issue
Block a user