` element.
+
+
+
+
diff --git a/docs-ui/src/content/components/text-field.mdx b/docs-ui/src/content/components/text-field.mdx
index 03307d2e36..ad90783ffb 100644
--- a/docs-ui/src/content/components/text-field.mdx
+++ b/docs-ui/src/content/components/text-field.mdx
@@ -8,11 +8,15 @@ import {
textFieldSizesSnippet,
textFieldDescriptionSnippet,
} from './text-field.props';
-import { ComponentInfos } from '@/components/ComponentInfos';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
-# TextField
-
-A text field component for your forms.
+
-
+## Usage
+
+
## API reference
@@ -66,3 +58,7 @@ Here's a simple TextField with a description.
preview={
}
code={textFieldDescriptionSnippet}
/>
+
+
+
+
diff --git a/docs-ui/src/content/components/text.mdx b/docs-ui/src/content/components/text.mdx
index 921a942dae..48b461aa8c 100644
--- a/docs-ui/src/content/components/text.mdx
+++ b/docs-ui/src/content/components/text.mdx
@@ -12,11 +12,14 @@ import {
textResponsiveSnippet,
textColorsSnippet,
} from './text.props';
-import { ComponentInfos } from '@/components/ComponentInfos';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
-# Text
-
-The `Text` component is used to display content on your page.
+
-
+## Usage
+
+
## API reference
@@ -103,3 +86,7 @@ You can also use the `variant` prop to change the appearance of the text based
on the screen size.
+
+
+
+
diff --git a/docs-ui/src/content/components/tooltip.mdx b/docs-ui/src/content/components/tooltip.mdx
index 4c231aab49..dd2add5308 100644
--- a/docs-ui/src/content/components/tooltip.mdx
+++ b/docs-ui/src/content/components/tooltip.mdx
@@ -8,11 +8,14 @@ import {
tooltipTriggerPropDefs,
tooltipPropDefs,
} from './tooltip.props';
-import { ComponentInfos } from '@/components/ComponentInfos';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
-# Tooltip
-
-A tooltip displays a description of an element on hover or focus.
+
-
+## Usage
+
+
## API reference
@@ -40,3 +41,7 @@ The trigger will wrap both the trigger and the tooltip.
The tooltip will wrap the content of the tooltip.
+
+
+
+
diff --git a/docs-ui/src/css/globals.css b/docs-ui/src/css/globals.css
index f8ac189113..334e6a870a 100644
--- a/docs-ui/src/css/globals.css
+++ b/docs-ui/src/css/globals.css
@@ -2,7 +2,8 @@
--bg: #f8f8f8;
--panel: #fff;
--primary: #000;
- --secondary: #757575;
+ --secondary: #929292;
+ --action: #fff;
--link: #4f5ce0;
--font-regular: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
@@ -17,17 +18,16 @@
[data-theme='dark'] {
--bg: #000;
- --panel: #1a1a1a;
+ --panel: #181818;
--primary: #fff;
--secondary: #818181;
+ --action: #202020;
--link: #4856e8;
--border: #2a2a2a;
--surface-1: #282828;
}
body {
- display: flex;
- flex-direction: row;
background-color: var(--bg);
color: var(--primary);
font-family: var(--font-regular);
diff --git a/docs-ui/src/css/mdx.module.css b/docs-ui/src/css/mdx.module.css
index 61c0208865..6a65995003 100644
--- a/docs-ui/src/css/mdx.module.css
+++ b/docs-ui/src/css/mdx.module.css
@@ -1,8 +1,9 @@
.h1 {
- font-size: 2.5rem;
- margin-top: 4rem;
- margin-bottom: 1.5rem;
+ font-size: 3rem;
+ margin-top: 0;
+ margin-bottom: 1.25rem;
color: var(--primary);
+ font-weight: 400;
}
.h2 {
@@ -10,6 +11,7 @@
margin-top: 3rem;
margin-bottom: 1.5rem;
color: var(--primary);
+ font-weight: 400;
}
.h3 {
@@ -17,6 +19,7 @@
margin-top: 2rem;
margin-bottom: 0.5rem;
color: var(--primary);
+ font-weight: 400;
}
.p {
diff --git a/docs-ui/src/css/page.module.css b/docs-ui/src/css/page.module.css
deleted file mode 100644
index 3a0bb492d8..0000000000
--- a/docs-ui/src/css/page.module.css
+++ /dev/null
@@ -1,50 +0,0 @@
-.global {
- width: 100%;
-}
-
-.container {
- width: 100%;
- padding: 0 16px;
-}
-
-@media (min-width: 768px) {
- .container {
- padding-left: 332px;
- }
-}
-
-.page {
- flex: 1;
-}
-
-.page p {
- color: var(--primary);
- font-size: 1rem;
- line-height: 1.5rem;
- margin-top: 0;
- margin-bottom: 1rem;
-}
-
-.content {
- width: 100%;
- max-width: 960px;
- margin: 0 auto;
- padding: 64px 24px 64px;
-}
-
-.header {
- width: 100%;
- margin-bottom: 64px;
-}
-
-.header img {
- width: 100%;
- height: auto;
-}
-
-.pageContainer {
- width: 100%;
- max-width: 960px;
- margin: 0 auto;
- padding: 0 24px 64px;
-}
diff --git a/docs-ui/yarn.lock b/docs-ui/yarn.lock
index 7fbef814a6..22531f1a3b 100644
--- a/docs-ui/yarn.lock
+++ b/docs-ui/yarn.lock
@@ -907,6 +907,15 @@ __metadata:
languageName: node
linkType: hard
+"@remixicon/react@npm:^4.6.0":
+ version: 4.6.0
+ resolution: "@remixicon/react@npm:4.6.0"
+ peerDependencies:
+ react: ">=18.2.0"
+ checksum: 10/d92c8131734062fca8d82d55dd8f3f4e28cb72c5eefda528c8aba5f3298f0c847342eecdbd7787d6da5af21b699d735f6cf27f151f0c982c1c9f06a78b235c94
+ languageName: node
+ linkType: hard
+
"@rtsao/scc@npm:^1.1.0":
version: 1.1.0
resolution: "@rtsao/scc@npm:1.1.0"
@@ -2302,6 +2311,7 @@ __metadata:
"@mdx-js/loader": "npm:^3.1.0"
"@mdx-js/react": "npm:^3.1.0"
"@next/mdx": "npm:15.3.4"
+ "@remixicon/react": "npm:^4.6.0"
"@storybook/react": "npm:^8.6.8"
"@types/mdx": "npm:^2.0.13"
"@types/node": "npm:^20"
@@ -2321,7 +2331,6 @@ __metadata:
prop-types: "npm:^15.8.1"
react: "npm:19.1.0"
react-dom: "npm:19.1.0"
- react-frame-component: "npm:^5.2.7"
shiki: "npm:^1.26.1"
storybook: "npm:^8.6.8"
typescript: "npm:^5"
@@ -5587,17 +5596,6 @@ __metadata:
languageName: node
linkType: hard
-"react-frame-component@npm:^5.2.7":
- version: 5.2.7
- resolution: "react-frame-component@npm:5.2.7"
- peerDependencies:
- prop-types: ^15.5.9
- react: ">= 16.3"
- react-dom: ">= 16.3"
- checksum: 10/bedee0c25075cbfb6d592991937640a9c8680f9017284e73ced338cc07348b3efc99121052b60ba0923d3d4ec1ed050348ddec452f059e0b947f6bff1d5bfe24
- languageName: node
- linkType: hard
-
"react-is@npm:^16.13.1":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
diff --git a/microsite/data/plugins/signadot.yaml b/microsite/data/plugins/signadot.yaml
new file mode 100644
index 0000000000..9b6b162901
--- /dev/null
+++ b/microsite/data/plugins/signadot.yaml
@@ -0,0 +1,10 @@
+---
+title: Signadot
+author: Signadot
+authorUrl: https://www.signadot.com/
+category: Kubernetes
+description: View and manage lightweight ephemeral Kubernetes environments (Sandboxes) for testing microservices in pull requests and local development.
+documentation: https://github.com/signadot/backstage-plugin
+iconUrl: https://avatars.githubusercontent.com/u/57196635
+npmPackageName: '@signadot-lib/backstage-plugin'
+addedDate: '2025-07-09'
diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md
index 690b469df3..163ade002e 100644
--- a/packages/ui/report.api.md
+++ b/packages/ui/report.api.md
@@ -29,7 +29,7 @@ import { Table as Table_2 } from '@tanstack/react-table';
import type { TabListProps as TabListProps_2 } from 'react-aria-components';
import type { TabPanelProps as TabPanelProps_2 } from 'react-aria-components';
import { TabProps } from 'react-aria-components';
-import type { TabsProps as TabsProps_2 } from 'react-aria-components';
+import { TabsProps as TabsProps_2 } from 'react-aria-components';
import { TdHTMLAttributes } from 'react';
import type { TextFieldProps as TextFieldProps_2 } from 'react-aria-components';
import { ThHTMLAttributes } from 'react';
@@ -1021,6 +1021,8 @@ export interface HeaderProps {
// (undocumented)
menuItems?: HeaderMenuItem[];
// (undocumented)
+ onTabSelectionChange?: TabsProps_2['onSelectionChange'];
+ // (undocumented)
tabs?: HeaderTab[];
// (undocumented)
title?: string;
diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx
index e6aae08eeb..4f85d9096d 100644
--- a/packages/ui/src/components/Header/Header.tsx
+++ b/packages/ui/src/components/Header/Header.tsx
@@ -50,7 +50,7 @@ export const Header = (props: HeaderProps) => {
/>
{tabs && (
-
+
{tabs?.map(tab => (
diff --git a/packages/ui/src/components/Header/types.ts b/packages/ui/src/components/Header/types.ts
index e1156c9a55..cf76929389 100644
--- a/packages/ui/src/components/Header/types.ts
+++ b/packages/ui/src/components/Header/types.ts
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import { TabsProps } from 'react-aria-components';
+
/**
* Props for the main Header component.
*
@@ -26,6 +28,7 @@ export interface HeaderProps {
customActions?: React.ReactNode;
menuItems?: HeaderMenuItem[];
tabs?: HeaderTab[];
+ onTabSelectionChange?: TabsProps['onSelectionChange'];
}
/**
diff --git a/packages/ui/src/components/Menu/Menu.stories.tsx b/packages/ui/src/components/Menu/Menu.stories.tsx
index 578ce945ae..c135b6f3f9 100644
--- a/packages/ui/src/components/Menu/Menu.stories.tsx
+++ b/packages/ui/src/components/Menu/Menu.stories.tsx
@@ -40,34 +40,33 @@ const options = [
];
export const Default: Story = {
- args: {
- children: (
- <>
- (
- }
- >
- Menu
-
- )}
- />
-
-
-
- Settings
- Invite new members
- Download app
- Log out
-
-
-
- >
- ),
- },
+ args: { children: undefined },
+ render: args => (
+
+ (
+ }
+ >
+ Menu
+
+ )}
+ />
+
+
+
+ Settings
+ Invite new members
+ Download app
+ Log out
+
+
+
+
+ ),
};
export const Open: Story = {
@@ -75,6 +74,7 @@ export const Open: Story = {
...Default.args,
open: true,
},
+ render: Default.render,
};
export const OpenOnHover: Story = {
@@ -82,49 +82,49 @@ export const OpenOnHover: Story = {
...Default.args,
openOnHover: true,
},
+ render: Default.render,
};
export const Submenu: Story = {
- args: {
- children: (
- <>
- (
- }
- >
- Menu
-
- )}
- />
-
-
-
- Settings
- Invite new members
- Download app
- Log out
-
- Submenu
-
-
-
- Submenu Item 1
- Submenu Item 2
- Submenu Item 3
-
-
-
-
-
-
-
- >
- ),
- },
+ args: { children: undefined },
+ render: args => (
+
+ (
+ }
+ >
+ Menu
+
+ )}
+ />
+
+
+
+ Settings
+ Invite new members
+ Download app
+ Log out
+
+ Submenu
+
+
+
+ Submenu Item 1
+ Submenu Item 2
+ Submenu Item 3
+
+
+
+
+
+
+
+
+ ),
};
export const SubmenuCombobox = () => {
diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md
index 8476eed695..1db7a8e5e4 100644
--- a/plugins/catalog-react/report-alpha.api.md
+++ b/plugins/catalog-react/report-alpha.api.md
@@ -372,18 +372,43 @@ export const EntityHeaderBlueprint: ExtensionBlueprint<{
name: undefined;
params: {
loader: () => Promise;
+ filter?: EntityPredicate | ((entity: Entity) => boolean);
};
- output: ConfigurableExtensionDataRef<
- JSX_2.Element,
- 'core.reactElement',
- {
- optional: true;
- }
- >;
+ output:
+ | ConfigurableExtensionDataRef<
+ (entity: Entity) => boolean,
+ 'catalog.entity-filter-function',
+ {
+ optional: true;
+ }
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-filter-expression',
+ {
+ optional: true;
+ }
+ >
+ | ConfigurableExtensionDataRef<
+ JSX_2.Element,
+ 'core.reactElement',
+ {
+ optional: true;
+ }
+ >;
inputs: {};
- config: {};
- configInput: {};
+ config: {
+ filter: EntityPredicate | undefined;
+ };
+ configInput: {
+ filter?: EntityPredicate | undefined;
+ };
dataRefs: {
+ filterFunction: ConfigurableExtensionDataRef<
+ (entity: Entity) => boolean,
+ 'catalog.entity-filter-function',
+ {}
+ >;
element: ConfigurableExtensionDataRef<
JSX_2.Element,
'core.reactElement',
diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx
index 431197e753..b83136dcd9 100644
--- a/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx
+++ b/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx
@@ -19,22 +19,44 @@ import {
coreExtensionData,
ExtensionBoundary,
} from '@backstage/frontend-plugin-api';
+import { EntityPredicate } from '../predicates/types';
+import { Entity } from '@backstage/catalog-model';
+import { resolveEntityFilterData } from './resolveEntityFilterData';
+import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema';
+import {
+ entityFilterExpressionDataRef,
+ entityFilterFunctionDataRef,
+} from './extensionData';
/** @alpha */
export const EntityHeaderBlueprint = createExtensionBlueprint({
kind: 'entity-header',
- attachTo: { id: 'page:catalog/entity', input: 'header' },
+ attachTo: { id: 'page:catalog/entity', input: 'headers' },
dataRefs: {
+ filterFunction: entityFilterFunctionDataRef,
element: coreExtensionData.reactElement,
},
- output: [coreExtensionData.reactElement.optional()],
+ config: {
+ schema: {
+ filter: z => createEntityPredicateSchema(z).optional(),
+ },
+ },
+ output: [
+ entityFilterFunctionDataRef.optional(),
+ entityFilterExpressionDataRef.optional(),
+ coreExtensionData.reactElement.optional(),
+ ],
*factory(
params: {
loader: () => Promise;
+ filter?: EntityPredicate | ((entity: Entity) => boolean);
},
- { node },
+ { node, config },
) {
- const { loader } = params;
+ const { loader, filter } = params;
+
+ yield* resolveEntityFilterData(filter, config, node);
+
if (loader) {
yield coreExtensionData.reactElement(
ExtensionBoundary.lazy(node, loader),
diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md
index 4e00a5185e..81ebd10c3b 100644
--- a/plugins/catalog/report-alpha.api.md
+++ b/plugins/catalog/report-alpha.api.md
@@ -1097,17 +1097,24 @@ const _default: FrontendPlugin<
}
>;
inputs: {
- header: ExtensionInput<
- ConfigurableExtensionDataRef<
- JSX_2.Element,
- 'core.reactElement',
- {
- optional: true;
- }
- >,
+ headers: ExtensionInput<
+ | ConfigurableExtensionDataRef<
+ (entity: Entity) => boolean,
+ 'catalog.entity-filter-function',
+ {
+ optional: true;
+ }
+ >
+ | ConfigurableExtensionDataRef<
+ JSX_2.Element,
+ 'core.reactElement',
+ {
+ optional: true;
+ }
+ >,
{
- singleton: true;
- optional: true;
+ singleton: false;
+ optional: false;
}
>;
contents: ExtensionInput<
diff --git a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx
index 750c8567ee..8f5838d545 100644
--- a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx
+++ b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx
@@ -102,13 +102,21 @@ export const EntityLayout = (props: EntityLayoutProps) => {
UNSTABLE_contextMenuOptions,
contextMenuItems,
children,
- header,
NotFoundComponent,
parentEntityRelations,
} = props;
const { kind } = useRouteRefParams(entityRouteRef);
const { entity, loading, error } = useAsyncEntity();
+ const header = props.header ?? (
+
+ );
+
const routes = useElementFilter(
children,
elements =>
@@ -142,14 +150,7 @@ export const EntityLayout = (props: EntityLayoutProps) => {
return (
- {header ?? (
-
- )}
+ {!loading && header}
{loading && }
diff --git a/plugins/catalog/src/alpha/filters.tsx b/plugins/catalog/src/alpha/filters.tsx
index fcfbc394f2..d72e299fbb 100644
--- a/plugins/catalog/src/alpha/filters.tsx
+++ b/plugins/catalog/src/alpha/filters.tsx
@@ -133,13 +133,14 @@ const catalogListCatalogFilter = CatalogFilterBlueprint.makeWithOverrides({
},
});
+// this is the default order that the filters will be applied in
export default [
- catalogTagCatalogFilter,
catalogKindCatalogFilter,
catalogTypeCatalogFilter,
- catalogModeCatalogFilter,
- catalogNamespaceCatalogFilter,
- catalogLifecycleCatalogFilter,
- catalogProcessingStatusCatalogFilter,
catalogListCatalogFilter,
+ catalogModeCatalogFilter,
+ catalogLifecycleCatalogFilter,
+ catalogTagCatalogFilter,
+ catalogProcessingStatusCatalogFilter,
+ catalogNamespaceCatalogFilter,
];
diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx
index 81ec0c5cb9..f0c5316a84 100644
--- a/plugins/catalog/src/alpha/pages.tsx
+++ b/plugins/catalog/src/alpha/pages.tsx
@@ -79,10 +79,10 @@ export const catalogPage = PageBlueprint.makeWithOverrides({
export const catalogEntityPage = PageBlueprint.makeWithOverrides({
name: 'entity',
inputs: {
- header: createExtensionInput(
- [EntityHeaderBlueprint.dataRefs.element.optional()],
- { singleton: true, optional: true },
- ),
+ headers: createExtensionInput([
+ EntityHeaderBlueprint.dataRefs.element.optional(),
+ EntityHeaderBlueprint.dataRefs.filterFunction.optional(),
+ ]),
contents: createExtensionInput([
coreExtensionData.reactElement,
coreExtensionData.routePath,
@@ -124,9 +124,19 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({
{ title: string; items: Array<(typeof inputs.contents)[0]> }
>;
- const header = inputs.header?.get(
- EntityHeaderBlueprint.dataRefs.element,
- );
+ // Get available headers, sorted by if they have a filter function or not.
+ // TODO(blam): we should really have priority or some specificity here which can be used to sort the headers.
+ // That can be done with embedding the priority in the dataRef alongside the filter function.
+ const headers = inputs.headers
+ .map(header => ({
+ element: header.get(EntityHeaderBlueprint.dataRefs.element),
+ filter: header.get(EntityHeaderBlueprint.dataRefs.filterFunction),
+ }))
+ .sort((a, b) => {
+ if (a.filter && !b.filter) return -1;
+ if (!a.filter && b.filter) return 1;
+ return 0;
+ });
let groups = Object.entries(defaultEntityContentGroups).reduce(
(rest, group) => {
@@ -169,6 +179,10 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({
? menuItems.filter(i => i.filter(entity)).map(i => i.element)
: [];
+ const header = headers.find(
+ h => !h.filter || h.filter(entity!),
+ )?.element;
+
return (