diff --git a/.changeset/light-meals-cover.md b/.changeset/light-meals-cover.md index 131454de07..16aa023c97 100644 --- a/.changeset/light-meals-cover.md +++ b/.changeset/light-meals-cover.md @@ -1,5 +1,8 @@ --- '@backstage/plugin-catalog-react': minor +'@backstage/plugin-kubernetes': minor +'@backstage/plugin-api-docs': minor +'@backstage/plugin-techdocs': minor '@backstage/plugin-catalog': minor --- diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index 559abdc3f4..9be7261a86 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -725,7 +725,7 @@ app: Notes: -- Icons for groups and tabs are resolved via the app's IconsApi. When using a string icon id (for example `"dashboard"`), ensure that the corresponding icon bundles are enabled/installed in your app (see the Icons blueprint in the frontend system documentation). +- Icons for groups and tabs are resolved via the app's IconsApi. When using a string icon id (for example `"dashboard"`), ensure that the corresponding icon bundles are enabled/installed in your app (see the [IconBundleBlueprint documentation](../../reference/frontend-plugin-api.iconbundleblueprint.md)). - Group icons are only rendered if `showIcons` is set to `true`. ### Overriding or disabling a tab's group (per extension) @@ -741,7 +741,7 @@ app: config: # Move this tab to a custom group you defined above group: custom - # Show an icon for this entity content page but only if + # Show an icon for this entity content page but only if `showIcons` is enabled for the `page:catalog/entity` extension icon: my-icon # Disassociate from any group and show as a standalone tab diff --git a/docs/frontend-system/building-plugins/03-common-extension-blueprints.md b/docs/frontend-system/building-plugins/03-common-extension-blueprints.md index bb6f91a5f5..e10b5fcce6 100644 --- a/docs/frontend-system/building-plugins/03-common-extension-blueprints.md +++ b/docs/frontend-system/building-plugins/03-common-extension-blueprints.md @@ -73,7 +73,7 @@ Avoid using `convertLegacyEntityCardExtension` from `@backstage/core-compat-api` Creates entity content to be displayed on the entity pages of the catalog plugin. Exported as `EntityContentBlueprint`. -Supports optional params such as `group` and `icon`to: +Supports optional params such as `group` and `icon` to: - group: string | false — associates the content with a tab group on the entity page (for example "overview", "quality", "deployment", or any custom id). You can override or disable this per-installation via app-config using `app.extensions[...].config.group`, where `false` removes the grouping. - icon: string — sets the tab icon. Note: when providing a string, the icon is looked up via the app's IconsApi; make sure icon bundles are enabled/installed in your app (see the Icons blueprint reference above) so that the icon id you use is available. diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index bc8017de5b..3503805394 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -47,7 +47,7 @@ import { pluginInfoResolver } from './pluginInfoResolver'; import { appModuleNav } from './modules/appModuleNav'; import devtoolsPlugin from '@backstage/plugin-devtools/alpha'; import { unprocessedEntitiesDevToolsContent } from '@backstage/plugin-catalog-unprocessed-entities/alpha'; -import { default as catalogPlugin } from '@backstage/plugin-catalog/alpha'; +import catalogPlugin from '@backstage/plugin-catalog/alpha'; import InfoIcon from '@material-ui/icons/Info'; /* diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index eff709a7a7..51a7f85952 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -152,15 +152,33 @@ export function convertLegacyEntityContentExtension( ): ExtensionDefinition; // @alpha -export const defaultEntityContentGroups: { - overview: string; - documentation: string; - development: string; - deployment: string; - operation: string; - observability: string; +export const defaultEntityContentGroupDefinitions: { + overview: { + title: string; + }; + documentation: { + title: string; + }; + development: { + title: string; + }; + deployment: { + title: string; + }; + operation: { + title: string; + }; + observability: { + title: string; + }; }; +// @alpha @deprecated +export const defaultEntityContentGroups: Record< + keyof typeof defaultEntityContentGroupDefinitions, + string +>; + // @alpha export const EntityCardBlueprint: ExtensionBlueprint<{ kind: 'entity-card'; @@ -604,6 +622,15 @@ export type EntityTableColumnTitleProps = { | 'domain'; }; +// @alpha (undocumented) +export type GroupDefinitions = Record< + string, + { + title: string; + icon?: string | ReactElement; + } +>; + // @alpha export function isOwnerOf(owner: Entity, entity: Entity): boolean; diff --git a/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx b/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx index bf85810ff5..936784410f 100644 --- a/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx @@ -41,18 +41,50 @@ export const entityFilterExpressionDataRef = id: 'catalog.entity-filter-expression', }); +/** @alpha */ +export type GroupDefinitions = Record< + string, + { + title: string; + icon?: string | ReactElement; + } +>; + /** * @alpha * Default entity content groups. */ -export const defaultEntityContentGroups = { - overview: 'Overview', - documentation: 'Documentation', - development: 'Development', - deployment: 'Deployment', - operation: 'Operation', - observability: 'Observability', -}; +export const defaultEntityContentGroupDefinitions = { + overview: { + title: 'Overview', + }, + documentation: { + title: 'Documentation', + }, + development: { + title: 'Development', + }, + deployment: { + title: 'Deployment', + }, + operation: { + title: 'Operation', + }, + observability: { + title: 'Observability', + }, +} satisfies GroupDefinitions; + +/** + * @alpha + * Default entity content groups. + * @deprecated use defaultEntityContentGroupDefinitions + */ +export const defaultEntityContentGroups = Object.fromEntries( + Object.entries(defaultEntityContentGroupDefinitions).map( + ([key, { title }]) => [key, title], + ), +) as Record; /** @internal */ export const entityContentGroupDataRef = createExtensionDataRef().with({ diff --git a/plugins/catalog-react/src/alpha/blueprints/index.ts b/plugins/catalog-react/src/alpha/blueprints/index.ts index 640ebb881c..d71b492063 100644 --- a/plugins/catalog-react/src/alpha/blueprints/index.ts +++ b/plugins/catalog-react/src/alpha/blueprints/index.ts @@ -21,7 +21,11 @@ export { type EntityContentLayoutProps, } from './EntityContentLayoutBlueprint'; export { EntityHeaderBlueprint } from './EntityHeaderBlueprint'; -export { defaultEntityContentGroups } from './extensionData'; +export { + defaultEntityContentGroups, + defaultEntityContentGroupDefinitions, + type GroupDefinitions, +} from './extensionData'; export type { EntityCardType } from './extensionData'; export { EntityContextMenuItemBlueprint, diff --git a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx index d02a0b1193..3f557861ec 100644 --- a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx @@ -40,11 +40,12 @@ import { import { catalogTranslationRef } from '../../translation'; import { EntityHeader } from '../EntityHeader'; import { EntityTabs } from '../EntityTabs'; +import { GroupDefinitions } from '@backstage/plugin-catalog-react/alpha'; export type EntityLayoutRouteProps = { path: string; title: string; - group: string | { title: string; icon?: string }; + group?: string; icon?: string | ReactElement; children: JSX.Element; if?: (entity: Entity) => boolean; @@ -78,6 +79,8 @@ export interface EntityLayoutProps { * It adds breadcrumbs in the Entity page to enhance user navigation and context awareness. */ parentEntityRelations?: string[]; + groupDefinitions: GroupDefinitions; + showIcons?: boolean; } /** @@ -106,6 +109,8 @@ export const EntityLayout = (props: EntityLayoutProps) => { header, NotFoundComponent, parentEntityRelations, + groupDefinitions, + showIcons, } = props; const { kind } = useRouteRefParams(entityRouteRef); const { entity, loading, error } = useAsyncEntity(); @@ -155,7 +160,13 @@ export const EntityLayout = (props: EntityLayoutProps) => { {loading && } - {entity && } + {entity && ( + + )} {error && ( diff --git a/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.tsx b/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.tsx index cb4c64300a..acc97d165e 100644 --- a/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.tsx +++ b/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.tsx @@ -18,9 +18,10 @@ import { Helmet } from 'react-helmet'; import { matchRoutes, useParams, useRoutes, Outlet } from 'react-router-dom'; import { EntityTabsPanel } from './EntityTabsPanel'; import { EntityTabsList } from './EntityTabsList'; +import { GroupDefinitions } from '@backstage/plugin-catalog-react/alpha'; type SubRoute = { - group: string | { title: string; icon?: string }; + group?: string; path: string; title: string; icon?: string | ReactElement; @@ -78,10 +79,12 @@ export function useSelectedSubRoute(subRoutes: SubRoute[]): { type EntityTabsProps = { routes: SubRoute[]; + groupDefinitions: GroupDefinitions; + showIcons?: boolean; }; export function EntityTabs(props: EntityTabsProps) { - const { routes } = props; + const { routes, groupDefinitions, showIcons } = props; const { index, route, element } = useSelectedSubRoute(routes); @@ -95,7 +98,7 @@ export function EntityTabs(props: EntityTabsProps) { // And remove leading / for relative navigation to = to.replace(/^\//, ''); return { - group: typeof group === 'string' ? { title: group } : group, + group, id: path, path: to, label: title, @@ -107,7 +110,12 @@ export function EntityTabs(props: EntityTabsProps) { return ( <> - + {element} diff --git a/plugins/catalog/src/alpha/components/EntityTabs/EntityTabsGroup.tsx b/plugins/catalog/src/alpha/components/EntityTabs/EntityTabsGroup.tsx index bc2e4f92b9..bc15b3fd05 100644 --- a/plugins/catalog/src/alpha/components/EntityTabs/EntityTabsGroup.tsx +++ b/plugins/catalog/src/alpha/components/EntityTabs/EntityTabsGroup.tsx @@ -142,7 +142,6 @@ const styles = (theme: Theme) => type EntityTabsGroupItem = { id: string; - index: number; label: string; path: string; icon?: string | ReactElement; @@ -151,15 +150,20 @@ type EntityTabsGroupItem = { type EntityTabsGroupProps = TabProps & { classes?: Partial>; indicator?: ReactNode; - highlightedButton?: number; + highlightedButton?: string; items: EntityTabsGroupItem[]; - onSelectTab: MouseEventHandler; + onSelectTab?: MouseEventHandler; + showIcons?: boolean; }; function resolveIcon( icon: string | ReactElement | undefined, iconsApi: IconsApi, + showIcons: boolean, ) { + if (!showIcons) { + return undefined; + } const itemIcon = icon; if (typeof itemIcon === 'string') { const Icon = iconsApi.getIcon(itemIcon); @@ -191,9 +195,10 @@ const Tab = forwardRef(function Tab(props: EntityTabsGroupProps, ref: any) { textColor = 'inherit', wrapped = false, highlightedButton, + showIcons = false, } = props; - const groupIcon = resolveIcon(props.icon, iconsApi); + const groupIcon = resolveIcon(props.icon, iconsApi, showIcons); const testId = 'data-testid' in props && props['data-testid']; const handleMenuClose = () => { @@ -235,7 +240,7 @@ const Tab = forwardRef(function Tab(props: EntityTabsGroupProps, ref: any) { component={Link} onClick={onSelectTab} to={items[0]?.path} - startIcon={resolveIcon(items[0].icon, iconsApi) ?? groupIcon} + startIcon={resolveIcon(items[0].icon, iconsApi, showIcons)} > {items[0].label} @@ -244,7 +249,7 @@ const Tab = forwardRef(function Tab(props: EntityTabsGroupProps, ref: any) { ); } - const hasIcons = items.some(i => i.icon); + const hasIcons = showIcons && items.some(i => i.icon); return ( <>