Merge branch 'backstage:master' into fix-gitlab-get-projectid

This commit is contained in:
Captain Fizzbin
2025-06-19 13:06:51 -04:00
committed by GitHub
57 changed files with 1274 additions and 941 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Fixed an issue causing TechDocs to not properly handle initial redirect.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-catalog-backend': major
'@backstage/create-app': patch
---
**BREAKING:** The experimental `catalog.useUrlReadersSearch` configuration flag (introduced in v1.36) has been removed.
The `UrlReaderProcessor` now always uses the `search` method of `UrlReaders`. Built-in `UrlReaderService` implementations have been updated accordingly.
If you use custom `UrlReaderService` implementations, you need to adapt their `search` method to correctly handle both specific URLs and potential
search patterns (see changes on built-in readers [in the original PR](https://github.com/backstage/backstage/pull/28379/files#diff-68b0452f173ee54bdd40f7b5e047a9cb8bb59200425622c212c217b76dac1d1b)).
Previous behavior was to call the `search` method only if the parsed Git URL's filename contained a wildcard and use `readUrl` otherwise. Each `UrlReaderService` must implement this logic in the `search` method instead.
This allows each `UrlReaderService` implementation to check whether it's a search URL (that contains a wildcard pattern) or not using logic that is specific to each provider.
+207
View File
@@ -0,0 +1,207 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"example-app": "0.2.110",
"@backstage/app-defaults": "1.6.3",
"example-app-next": "0.0.24",
"app-next-example-plugin": "0.0.24",
"example-backend": "0.0.39",
"@backstage/backend-app-api": "1.2.4",
"@backstage/backend-defaults": "0.11.0",
"@backstage/backend-dev-utils": "0.1.5",
"@backstage/backend-dynamic-feature-service": "0.7.1",
"@backstage/backend-openapi-utils": "0.5.4",
"@backstage/backend-plugin-api": "1.4.0",
"@backstage/backend-test-utils": "1.6.0",
"@backstage/canon": "0.5.0",
"@backstage/catalog-client": "1.10.1",
"@backstage/catalog-model": "1.7.4",
"@backstage/cli": "0.33.0",
"@backstage/cli-common": "0.1.15",
"@backstage/cli-node": "0.2.13",
"@backstage/codemods": "0.1.52",
"@backstage/config": "1.3.2",
"@backstage/config-loader": "1.10.1",
"@backstage/core-app-api": "1.17.1",
"@backstage/core-compat-api": "0.4.3",
"@backstage/core-components": "0.17.3",
"@backstage/core-plugin-api": "1.10.8",
"@backstage/create-app": "0.7.0",
"@backstage/dev-utils": "1.1.11",
"e2e-test": "0.2.29",
"@backstage/e2e-test-utils": "0.1.1",
"@backstage/errors": "1.2.7",
"@backstage/eslint-plugin": "0.1.11",
"@backstage/frontend-app-api": "0.11.3",
"@backstage/frontend-defaults": "0.2.3",
"@backstage/frontend-dynamic-feature-loader": "0.1.2",
"@internal/frontend": "0.0.10",
"@backstage/frontend-plugin-api": "0.10.3",
"@backstage/frontend-test-utils": "0.3.3",
"@backstage/integration": "1.17.0",
"@backstage/integration-aws-node": "0.1.16",
"@backstage/integration-react": "1.2.8",
"@internal/opaque": "0.0.1",
"@backstage/release-manifests": "0.0.13",
"@backstage/repo-tools": "0.14.0",
"@internal/scaffolder": "0.0.10",
"@techdocs/cli": "1.9.4",
"techdocs-cli-embedded-app": "0.2.109",
"@backstage/test-utils": "1.7.9",
"@backstage/theme": "0.6.6",
"@backstage/types": "1.2.1",
"@backstage/version-bridge": "1.0.11",
"yarn-plugin-backstage": "0.0.6",
"@backstage/plugin-api-docs": "0.12.8",
"@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.10",
"@backstage/plugin-app": "0.1.10",
"@backstage/plugin-app-backend": "0.5.3",
"@backstage/plugin-app-node": "0.1.34",
"@backstage/plugin-app-visualizer": "0.1.20",
"@backstage/plugin-auth-backend": "0.25.1",
"@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.4",
"@backstage/plugin-auth-backend-module-auth0-provider": "0.2.4",
"@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.4",
"@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.9",
"@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.4",
"@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.4",
"@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.4",
"@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.4",
"@backstage/plugin-auth-backend-module-github-provider": "0.3.4",
"@backstage/plugin-auth-backend-module-gitlab-provider": "0.3.4",
"@backstage/plugin-auth-backend-module-google-provider": "0.3.4",
"@backstage/plugin-auth-backend-module-guest-provider": "0.2.9",
"@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.4",
"@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.4",
"@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.9",
"@backstage/plugin-auth-backend-module-oidc-provider": "0.4.4",
"@backstage/plugin-auth-backend-module-okta-provider": "0.2.4",
"@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.4",
"@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.4",
"@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.4",
"@backstage/plugin-auth-node": "0.6.4",
"@backstage/plugin-auth-react": "0.1.16",
"@backstage/plugin-bitbucket-cloud-common": "0.3.0",
"@backstage/plugin-catalog": "1.31.0",
"@backstage/plugin-catalog-backend": "2.1.0",
"@backstage/plugin-catalog-backend-module-aws": "0.4.12",
"@backstage/plugin-catalog-backend-module-azure": "0.3.6",
"@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.3",
"@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.0",
"@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.0",
"@backstage/plugin-catalog-backend-module-gcp": "0.3.9",
"@backstage/plugin-catalog-backend-module-gerrit": "0.3.3",
"@backstage/plugin-catalog-backend-module-gitea": "0.1.1",
"@backstage/plugin-catalog-backend-module-github": "0.10.0",
"@backstage/plugin-catalog-backend-module-github-org": "0.3.11",
"@backstage/plugin-catalog-backend-module-gitlab": "0.7.0",
"@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.10",
"@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.1",
"@backstage/plugin-catalog-backend-module-ldap": "0.11.6",
"@backstage/plugin-catalog-backend-module-logs": "0.1.11",
"@backstage/plugin-catalog-backend-module-msgraph": "0.7.1",
"@backstage/plugin-catalog-backend-module-openapi": "0.2.11",
"@backstage/plugin-catalog-backend-module-puppetdb": "0.2.11",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.9",
"@backstage/plugin-catalog-backend-module-unprocessed": "0.6.1",
"@backstage/plugin-catalog-common": "1.1.4",
"@backstage/plugin-catalog-graph": "0.4.20",
"@backstage/plugin-catalog-import": "0.13.1",
"@backstage/plugin-catalog-node": "1.17.1",
"@backstage/plugin-catalog-react": "1.19.0",
"@backstage/plugin-catalog-unprocessed-entities": "0.2.18",
"@backstage/plugin-catalog-unprocessed-entities-common": "0.0.8",
"@backstage/plugin-config-schema": "0.1.69",
"@backstage/plugin-devtools": "0.1.28",
"@backstage/plugin-devtools-backend": "0.5.6",
"@backstage/plugin-devtools-common": "0.1.16",
"@backstage/plugin-events-backend": "0.5.3",
"@backstage/plugin-events-backend-module-aws-sqs": "0.4.12",
"@backstage/plugin-events-backend-module-azure": "0.2.21",
"@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.21",
"@backstage/plugin-events-backend-module-bitbucket-server": "0.1.2",
"@backstage/plugin-events-backend-module-gerrit": "0.2.21",
"@backstage/plugin-events-backend-module-github": "0.4.1",
"@backstage/plugin-events-backend-module-gitlab": "0.3.2",
"@backstage/plugin-events-backend-module-google-pubsub": "0.1.1",
"@backstage/plugin-events-backend-module-kafka": "0.1.0",
"@backstage/plugin-events-backend-test-utils": "0.1.45",
"@backstage/plugin-events-node": "0.4.12",
"@internal/plugin-todo-list": "1.0.40",
"@internal/plugin-todo-list-backend": "1.0.40",
"@internal/plugin-todo-list-common": "1.0.25",
"@backstage/plugin-gateway-backend": "1.0.2",
"@backstage/plugin-home": "0.8.9",
"@backstage/plugin-home-react": "0.1.27",
"@backstage/plugin-kubernetes": "0.12.8",
"@backstage/plugin-kubernetes-backend": "0.19.7",
"@backstage/plugin-kubernetes-cluster": "0.0.26",
"@backstage/plugin-kubernetes-common": "0.9.5",
"@backstage/plugin-kubernetes-node": "0.3.1",
"@backstage/plugin-kubernetes-react": "0.5.8",
"@backstage/plugin-mcp-actions-backend": "0.1.0",
"@backstage/plugin-notifications": "0.5.6",
"@backstage/plugin-notifications-backend": "0.5.7",
"@backstage/plugin-notifications-backend-module-email": "0.3.10",
"@backstage/plugin-notifications-backend-module-slack": "0.1.2",
"@backstage/plugin-notifications-common": "0.0.9",
"@backstage/plugin-notifications-node": "0.2.16",
"@backstage/plugin-org": "0.6.40",
"@backstage/plugin-org-react": "0.1.39",
"@backstage/plugin-permission-backend": "0.7.1",
"@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.9",
"@backstage/plugin-permission-common": "0.9.0",
"@backstage/plugin-permission-node": "0.10.1",
"@backstage/plugin-permission-react": "0.4.35",
"@backstage/plugin-proxy-backend": "0.6.3",
"@backstage/plugin-proxy-node": "0.1.5",
"@backstage/plugin-scaffolder": "1.32.0",
"@backstage/plugin-scaffolder-backend": "2.0.0",
"@backstage/plugin-scaffolder-backend-module-azure": "0.2.10",
"@backstage/plugin-scaffolder-backend-module-bitbucket": "0.3.11",
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.2.10",
"@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.10",
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.10",
"@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.11",
"@backstage/plugin-scaffolder-backend-module-gcp": "0.2.10",
"@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.10",
"@backstage/plugin-scaffolder-backend-module-gitea": "0.2.10",
"@backstage/plugin-scaffolder-backend-module-github": "0.8.0",
"@backstage/plugin-scaffolder-backend-module-gitlab": "0.9.2",
"@backstage/plugin-scaffolder-backend-module-notifications": "0.1.11",
"@backstage/plugin-scaffolder-backend-module-rails": "0.5.10",
"@backstage/plugin-scaffolder-backend-module-sentry": "0.2.10",
"@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.11",
"@backstage/plugin-scaffolder-common": "1.5.11",
"@backstage/plugin-scaffolder-node": "0.9.0",
"@backstage/plugin-scaffolder-node-test-utils": "0.3.0",
"@backstage/plugin-scaffolder-react": "1.17.0",
"@backstage/plugin-search": "1.4.27",
"@backstage/plugin-search-backend": "2.0.3",
"@backstage/plugin-search-backend-module-catalog": "0.3.5",
"@backstage/plugin-search-backend-module-elasticsearch": "1.7.3",
"@backstage/plugin-search-backend-module-explore": "0.3.3",
"@backstage/plugin-search-backend-module-pg": "0.5.45",
"@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.10",
"@backstage/plugin-search-backend-module-techdocs": "0.4.3",
"@backstage/plugin-search-backend-node": "1.3.12",
"@backstage/plugin-search-common": "1.2.18",
"@backstage/plugin-search-react": "1.9.1",
"@backstage/plugin-signals": "0.0.20",
"@backstage/plugin-signals-backend": "0.3.5",
"@backstage/plugin-signals-node": "0.1.21",
"@backstage/plugin-signals-react": "0.0.14",
"@backstage/plugin-techdocs": "1.13.0",
"@backstage/plugin-techdocs-addons-test-utils": "1.0.49",
"@backstage/plugin-techdocs-backend": "2.0.3",
"@backstage/plugin-techdocs-common": "0.1.1",
"@backstage/plugin-techdocs-module-addons-contrib": "1.1.25",
"@backstage/plugin-techdocs-node": "1.13.4",
"@backstage/plugin-techdocs-react": "1.3.0",
"@backstage/plugin-user-settings": "0.8.23",
"@backstage/plugin-user-settings-backend": "0.3.3",
"@backstage/plugin-user-settings-common": "0.0.1"
},
"changesets": []
}
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/canon': minor
---
**BREAKING CHANGES**
Were updating our Button component to provide better support for button links.
- Were introducing a new `ButtonLink` component, which replaces the previous render prop pattern.
- To maintain naming consistency across components, `IconButton` is being renamed to `ButtonIcon`.
- Additionally, the render prop will be removed from all button-related components.
These changes aim to simplify usage and improve clarity in our component API.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-mcp-actions-backend': patch
---
Fixed the example in the README for generating a static token by adding a subject field
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch
---
Fixing the typescript issue with using `z.unknown()`
-1
View File
@@ -138,7 +138,6 @@ integrations:
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
catalog:
useUrlReadersSearch: true
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
@@ -55,10 +55,6 @@
gap: 2px;
}
.playgroundSection {
position: absolute;
}
.sectionTitle {
font-size: var(--canon-font-size-3);
font-weight: var(--canon-font-weight-bold);
@@ -38,9 +38,7 @@ export const Playground = () => {
return (
<motion.div
className={`${styles.section} ${
isPlayground ? styles.playgroundSection : ''
}`}
className={styles.section}
animate={{
opacity: isPlayground ? 1 : 0,
x: isPlayground ? 0 : 20,
@@ -52,6 +50,7 @@ export const Playground = () => {
visibility: isPlayground ? 'visible' : 'hidden',
}}
transition={{ duration: 0.2 }}
style={{ position: 'absolute' }}
>
<div className={styles.sectionTitle}>Components</div>
{components.map(({ slug, title }) => (
@@ -0,0 +1,80 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ButtonIconSnippet } from '@/snippets/stories-snippets';
import {
buttonIconPropDefs,
buttonIconUsageSnippet,
buttonIconDefaultSnippet,
buttonIconVariantsSnippet,
buttonIconSizesSnippet,
buttonIconDisabledSnippet,
buttonIconResponsiveSnippet,
buttonIconAsLinkSnippet,
} from './button-icon.props';
import { ComponentInfos } from '@/components/ComponentInfos';
# ButtonIcon
A button component with a single icon that can be used to trigger actions.
<Snippet
align="center"
py={4}
preview={<ButtonIconSnippet story="Variants" />}
code={buttonIconDefaultSnippet}
/>
<ComponentInfos
component="button-icon"
classNames={['canon-Button', 'canon-ButtonIcon']}
usageCode={buttonIconUsageSnippet}
/>
## API reference
<PropsTable data={buttonIconPropDefs} />
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<ButtonIconSnippet story="Variants" />}
code={buttonIconVariantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<ButtonIconSnippet story="Sizes" />}
code={buttonIconSizesSnippet}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<ButtonIconSnippet story="Disabled" />}
code={buttonIconDisabledSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={buttonIconResponsiveSnippet} />
@@ -0,0 +1,59 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const buttonIconPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'medium',
responsive: true,
},
icon: { type: 'enum', values: ['ReactNode'], responsive: false },
isDisabled: { type: 'boolean', default: 'false', responsive: false },
type: {
type: 'enum',
values: ['button', 'submit', 'reset'],
default: 'button',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/canon';
<ButtonIcon />`;
export const buttonIconDefaultSnippet = `<Flex align="center">
<ButtonIcon icon={<Icon name="cloud" />} variant="primary" />
<ButtonIcon icon={<Icon name="cloud" />} variant="secondary" />
</Flex>`;
export const buttonIconVariantsSnippet = `<Flex align="center">
<ButtonIcon icon={<Icon name="cloud" />} variant="primary" />
<ButtonIcon icon={<Icon name="cloud" />} variant="secondary" />
</Flex>`;
export const buttonIconSizesSnippet = `<Flex align="center">
<ButtonIcon icon={<Icon name="cloud" />} size="small" />
<ButtonIcon icon={<Icon name="cloud" />} size="medium" />
</Flex>`;
export const buttonIconDisabledSnippet = `<ButtonIcon icon={<Icon name="cloud" />} isDisabled />`;
export const buttonIconResponsiveSnippet = `<ButtonIcon icon={<Icon name="cloud" />} variant={{ initial: 'primary', lg: 'secondary' }} />`;
export const buttonIconAsLinkSnippet = `import { ButtonLink } from '@backstage/canon';
<ButtonLink href="https://canon.backstage.io" target="_blank">
Button
</ButtonLink>`;
@@ -0,0 +1,91 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ButtonLinkSnippet } from '@/snippets/stories-snippets';
import {
buttonLinkPropDefs,
buttonLinkSnippetUsage,
buttonLinkVariantsSnippet,
buttonLinkSizesSnippet,
buttonLinkIconsSnippet,
buttonLinkDisabledSnippet,
buttonLinkResponsiveSnippet,
} from './button-link.props';
import { ComponentInfos } from '@/components/ComponentInfos';
# ButtonLink
A button component that can be used as a link.
<Snippet
align="center"
py={4}
preview={<ButtonLinkSnippet story="Variants" />}
code={buttonLinkVariantsSnippet}
/>
<ComponentInfos
component="button-link"
classNames={['canon-Button', 'canon-ButtonLink']}
usageCode={buttonLinkSnippetUsage}
/>
## API reference
<PropsTable data={buttonLinkPropDefs} />
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkSnippet story="Variants" />}
code={buttonLinkVariantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkSnippet story="Sizes" />}
code={buttonLinkSizesSnippet}
/>
### With Icons
Here's a view when buttons have icons.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkSnippet story="WithIcons" />}
code={buttonLinkIconsSnippet}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkSnippet story="Disabled" />}
code={buttonLinkDisabledSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={buttonLinkResponsiveSnippet} />
@@ -0,0 +1,73 @@
import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs';
import type { PropDef } from '../../utils/propDefs';
export const buttonLinkPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'medium',
responsive: true,
},
iconStart: { type: 'enum', values: ['ReactNode'], responsive: false },
iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false },
isDisabled: { type: 'boolean', default: 'false', responsive: false },
href: { type: 'string', responsive: false },
hrefLang: { type: 'string', responsive: false },
target: {
type: 'enum',
values: ['HTMLAttributeAnchorTarget'],
default: '_self',
responsive: false,
},
rel: { type: 'string', responsive: false },
children: { type: 'enum', values: ['ReactNode'], responsive: false },
...classNamePropDefs,
...stylePropDefs,
};
export const buttonLinkSnippetUsage = `import { ButtonLink } from '@backstage/canon';
<ButtonLink />`;
export const buttonLinkVariantsSnippet = `<Flex align="center">
<ButtonLink iconStart={<Icon name="cloud" />} variant="primary">
Button
</ButtonLink>
<ButtonLink iconStart={<Icon name="cloud" />} variant="secondary">
Button
</ButtonLink>
</Flex>`;
export const buttonLinkSizesSnippet = `<Flex align="center">
<ButtonLink size="small">Small</ButtonLink>
<ButtonLink size="medium">Medium</ButtonLink>
</Flex>`;
export const buttonLinkIconsSnippet = `<Flex align="center">
<ButtonLink iconStart={<Icon name="cloud" />}>Button</ButtonLink>
<ButtonLink iconEnd={<Icon name="chevronRight" />}>Button</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
iconEnd={<Icon name="chevronRight" />}>
Button
</ButtonLink>
</Flex>`;
export const buttonLinkDisabledSnippet = `<Flex gap="4">
<ButtonLink variant="primary" isDisabled>
Primary
</ButtonLink>
<ButtonLink variant="secondary" isDisabled>
Secondary
</ButtonLink>
</Flex>`;
export const buttonLinkResponsiveSnippet = `<ButtonLink variant={{ initial: 'primary', lg: 'secondary' }}>
Responsive Button
</ButtonLink>`;
+15 -15
View File
@@ -1,16 +1,16 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ButtonSnippet } from '@/snippets/stories-snippets';
import { ButtonSnippet, ButtonLinkSnippet } from '@/snippets/stories-snippets';
import {
buttonPropDefs,
buttonSnippetUsage,
buttonVariantsSnippet,
buttonSizesSnippet,
buttonIconsSnippet,
buttonFullWidthSnippet,
buttonDisabledSnippet,
buttonResponsiveSnippet,
buttonAsLinkSnippet,
} from './button.props';
import { ComponentInfos } from '@/components/ComponentInfos';
@@ -27,7 +27,7 @@ A button component that can be used to trigger actions.
<ComponentInfos
component="button"
classNames={['canon-Button', 'canon-ButtonIcon']}
classNames={['canon-Button']}
usageCode={buttonSnippetUsage}
/>
@@ -73,18 +73,6 @@ Here's a view when buttons have icons.
code={buttonIconsSnippet}
/>
### Full width
Here's a view when buttons are full width.
<Snippet
align="center"
py={4}
open
preview={<ButtonSnippet story="FullWidth" />}
code={buttonFullWidthSnippet}
/>
### Disabled
Here's a view when buttons are disabled.
@@ -102,3 +90,15 @@ Here's a view when buttons are disabled.
Here's a view when buttons are responsive.
<CodeBlock code={buttonResponsiveSnippet} />
### As Link
If you want to use a button as a link, please use the `ButtonLink` component.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkSnippet story="Variants" />}
code={buttonAsLinkSnippet}
/>
@@ -14,6 +14,16 @@ export const buttonPropDefs: Record<string, PropDef> = {
default: 'medium',
responsive: true,
},
iconStart: { type: 'enum', values: ['ReactNode'], responsive: false },
iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false },
isDisabled: { type: 'boolean', default: 'false', responsive: false },
children: { type: 'enum', values: ['ReactNode'], responsive: false },
type: {
type: 'enum',
values: ['button', 'submit', 'reset'],
default: 'button',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -29,9 +39,6 @@ export const buttonVariantsSnippet = `<Flex align="center">
<Button iconStart="cloud" variant="secondary">
Button
</Button>
<Button iconStart="cloud" variant="tertiary">
Button
</Button>
</Flex>`;
export const buttonSizesSnippet = `<Flex align="center">
@@ -40,20 +47,16 @@ export const buttonSizesSnippet = `<Flex align="center">
</Flex>`;
export const buttonIconsSnippet = `<Flex align="center">
<Button iconStart="cloud">Button</Button>
<Button iconEnd="chevronRight">Button</Button>
<Button iconStart="cloud" iconEnd="chevronRight">Button</Button>
</Flex>`;
export const buttonFullWidthSnippet = `<Flex direction="column" gap="4" style={{ width: '300px' }}>
<Button fullWidth>Full width</Button>
<Button iconStart={<Icon name="cloud" />}>Button</Button>
<Button iconEnd={<Icon name="chevronRight" />}>Button</Button>
<Button iconStart={<Icon name="cloud" />} iconEnd={<Icon name="chevronRight" />}>Button</Button>
</Flex>`;
export const buttonDisabledSnippet = `<Flex gap="4">
<Button variant="primary" disabled>
<Button variant="primary" isDisabled>
Primary
</Button>
<Button variant="secondary" disabled>
<Button variant="secondary" isDisabled>
Secondary
</Button>
</Flex>`;
@@ -61,3 +64,9 @@ export const buttonDisabledSnippet = `<Flex gap="4">
export const buttonResponsiveSnippet = `<Button variant={{ initial: 'primary', lg: 'secondary' }}>
Responsive Button
</Button>`;
export const buttonAsLinkSnippet = `import { ButtonLink } from '@backstage/canon';
<ButtonLink href="https://canon.backstage.io" target="_blank">
Button
</ButtonLink>`;
@@ -1,79 +0,0 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { IconButtonSnippet } from '@/snippets/stories-snippets';
import {
iconButtonPropDefs,
iconButtonUsageSnippet,
iconButtonDefaultSnippet,
iconButtonVariantsSnippet,
iconButtonSizesSnippet,
iconButtonDisabledSnippet,
iconButtonResponsiveSnippet,
} from './icon-button.props';
import { ComponentInfos } from '@/components/ComponentInfos';
# Icon Button
A button component with a single icon that can be used to trigger actions.
<Snippet
align="center"
py={4}
preview={<IconButtonSnippet story="Variants" />}
code={iconButtonDefaultSnippet}
/>
<ComponentInfos
component="icon-button"
classNames={['canon-IconButton']}
usageCode={iconButtonUsageSnippet}
/>
## API reference
<PropsTable data={iconButtonPropDefs} />
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<IconButtonSnippet story="Variants" />}
code={iconButtonVariantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<IconButtonSnippet story="Sizes" />}
code={iconButtonSizesSnippet}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<IconButtonSnippet story="Disabled" />}
code={iconButtonDisabledSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={iconButtonResponsiveSnippet} />
@@ -1,46 +0,0 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const iconButtonPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'medium',
responsive: true,
},
icon: { type: 'enum', values: 'icon', responsive: false },
...classNamePropDefs,
...stylePropDefs,
};
export const iconButtonUsageSnippet = `import { IconButton } from '@backstage/canon';
<IconButton />`;
export const iconButtonDefaultSnippet = `<Flex align="center">
<IconButton icon="cloud" variant="primary" />
<IconButton icon="cloud" variant="secondary" />
</Flex>`;
export const iconButtonVariantsSnippet = `<Flex align="center">
<IconButton icon="cloud" variant="primary" />
<IconButton icon="cloud" variant="secondary" />
</Flex>`;
export const iconButtonSizesSnippet = `<Flex align="center">
<IconButton icon="cloud" size="small" />
<IconButton icon="cloud" size="medium" />
</Flex>`;
export const iconButtonDisabledSnippet = `<IconButton icon="cloud" disabled />`;
export const iconButtonResponsiveSnippet = `<IconButton icon="cloud" variant={{ initial: 'primary', lg: 'secondary' }} />`;
+24 -12
View File
@@ -3,11 +3,12 @@
import { composeStories } from '@storybook/react';
import * as BoxStories from '../../../packages/canon/src/components/Box/Box.stories';
import * as ButtonStories from '../../../packages/canon/src/components/Button/Button.stories';
import * as ButtonIconStories from '../../../packages/canon/src/components/ButtonIcon/ButtonIcon.stories';
import * as ButtonLinkStories from '../../../packages/canon/src/components/ButtonLink/ButtonLink.stories';
import * as CheckboxStories from '../../../packages/canon/src/components/Checkbox/Checkbox.stories';
import * as ContainerStories from '../../../packages/canon/src/components/Container/Container.stories';
import * as GridStories from '../../../packages/canon/src/components/Grid/Grid.stories';
import * as HeadingStories from '../../../packages/canon/src/components/Heading/Heading.stories';
import * as IconButtonStories from '../../../packages/canon/src/components/IconButton/IconButton.stories';
import * as IconStories from '../../../packages/canon/src/components/Icon/Icon.stories';
import * as TextFieldStories from '../../../packages/canon/src/components/TextField/TextField.stories';
import * as TextStories from '../../../packages/canon/src/components/Text/Text.stories';
@@ -38,6 +39,28 @@ export const ButtonSnippet = ({
return StoryComponent ? <StoryComponent /> : null;
};
export const ButtonIconSnippet = ({
story,
}: {
story: keyof typeof ButtonIconStories;
}) => {
const stories = composeStories(ButtonIconStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const ButtonLinkSnippet = ({
story,
}: {
story: keyof typeof ButtonLinkStories;
}) => {
const stories = composeStories(ButtonLinkStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const CheckboxSnippet = ({
story,
}: {
@@ -96,17 +119,6 @@ export const HeadingSnippet = ({
return StoryComponent ? <StoryComponent /> : null;
};
export const IconButtonSnippet = ({
story,
}: {
story: keyof typeof IconButtonStories;
}) => {
const stories = composeStories(IconButtonStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const IconSnippet = ({ story }: { story: keyof typeof IconStories }) => {
const stories = composeStories(IconStories);
const StoryComponent = stories[story as keyof typeof stories];
+10 -5
View File
@@ -76,6 +76,16 @@ export const components: Page[] = [
slug: 'button',
status: 'alpha',
},
{
title: 'ButtonIcon',
slug: 'button-icon',
status: 'alpha',
},
{
title: 'ButtonLink',
slug: 'button-link',
status: 'alpha',
},
{
title: 'Checkbox',
slug: 'checkbox',
@@ -96,11 +106,6 @@ export const components: Page[] = [
slug: 'icon',
status: 'alpha',
},
{
title: 'IconButton',
slug: 'icon-button',
status: 'alpha',
},
{
title: 'Link',
slug: 'link',
+4 -4
View File
@@ -69,21 +69,21 @@
.canon-Button[data-size="medium"] {
font-size: var(--canon-font-size-4);
padding: 0 var(--canon-space-3);
height: 40px;
height: 2.5rem;
}
.canon-Button[data-size="small"] {
font-size: var(--canon-font-size-3);
padding: 0 var(--canon-space-2);
height: 32px;
height: 2rem;
}
.canon-ButtonIcon[data-size="small"], .canon-ButtonIcon[data-size="small"] svg {
.canon-Button[data-size="small"] svg {
width: 1rem;
height: 1rem;
}
.canon-ButtonIcon[data-size="medium"], .canon-ButtonIcon[data-size="medium"] svg {
.canon-Button[data-size="medium"] svg {
width: 1.25rem;
height: 1.25rem;
}
+14
View File
@@ -0,0 +1,14 @@
.canon-ButtonIcon {
justify-content: center;
align-items: center;
}
.canon-ButtonIcon[data-size="small"] {
width: 2rem;
padding: 0;
}
.canon-ButtonIcon[data-size="medium"] {
width: 2.5rem;
padding: 0;
}
+19 -94
View File
@@ -125,25 +125,40 @@
.canon-Button[data-size="medium"] {
font-size: var(--canon-font-size-4);
padding: 0 var(--canon-space-3);
height: 40px;
height: 2.5rem;
}
.canon-Button[data-size="small"] {
font-size: var(--canon-font-size-3);
padding: 0 var(--canon-space-2);
height: 32px;
height: 2rem;
}
.canon-ButtonIcon[data-size="small"], .canon-ButtonIcon[data-size="small"] svg {
.canon-Button[data-size="small"] svg {
width: 1rem;
height: 1rem;
}
.canon-ButtonIcon[data-size="medium"], .canon-ButtonIcon[data-size="medium"] svg {
.canon-Button[data-size="medium"] svg {
width: 1.25rem;
height: 1.25rem;
}
.canon-ButtonIcon {
justify-content: center;
align-items: center;
}
.canon-ButtonIcon[data-size="small"] {
width: 2rem;
padding: 0;
}
.canon-ButtonIcon[data-size="medium"] {
width: 2.5rem;
padding: 0;
}
.canon-CheckboxRoot {
width: 1rem;
height: 1rem;
@@ -351,96 +366,6 @@
height: 1rem;
}
.canon-IconButton {
user-select: none;
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-bold);
cursor: pointer;
border-radius: var(--canon-radius-2);
justify-content: center;
align-items: center;
gap: var(--canon-space-1_5);
border: none;
padding: 0;
display: inline-flex;
&:disabled {
cursor: not-allowed;
}
}
.canon-IconButton[data-variant="primary"] {
background-color: var(--canon-bg-solid);
color: var(--canon-fg-solid);
transition: background-color .15s, box-shadow .15s;
&:hover {
background-color: var(--canon-bg-solid-hover);
}
&:active {
background-color: var(--canon-bg-solid-pressed);
}
&:focus-visible {
outline: 2px solid var(--canon-ring);
outline-offset: 2px;
}
&:disabled {
background-color: var(--canon-bg-solid-disabled);
color: var(--canon-fg-solid-disabled);
}
}
.canon-IconButton[data-variant="secondary"] {
background-color: var(--canon-bg-surface-1);
box-shadow: inset 0 0 0 1px var(--canon-border);
color: var(--canon-fg-primary);
transition: box-shadow .15s;
&:hover {
box-shadow: inset 0 0 0 1px var(--canon-border-hover);
}
&:active {
box-shadow: inset 0 0 0 1px var(--canon-border-pressed);
}
&:focus-visible {
box-shadow: inset 0 0 0 2px var(--canon-ring);
outline: none;
transition: none;
}
&:disabled {
box-shadow: inset 0 0 0 1px var(--canon-border-disabled);
color: var(--canon-fg-disabled);
}
}
.canon-IconButton[data-size="medium"] {
font-size: var(--canon-font-size-4);
width: 40px;
height: 40px;
}
.canon-IconButton[data-size="small"] {
font-size: var(--canon-font-size-3);
width: 32px;
height: 32px;
}
.canon-IconButtonIcon[data-size="small"], .canon-IconButtonIcon[data-size="small"] svg {
width: 1rem;
height: 1rem;
}
.canon-IconButtonIcon[data-size="medium"], .canon-IconButtonIcon[data-size="medium"] svg {
width: 1.25rem;
height: 1.25rem;
}
.canon-Link {
font-family: var(--canon-font-regular);
color: var(--canon-fg-link);
+19 -94
View File
@@ -9349,25 +9349,40 @@
.canon-Button[data-size="medium"] {
font-size: var(--canon-font-size-4);
padding: 0 var(--canon-space-3);
height: 40px;
height: 2.5rem;
}
.canon-Button[data-size="small"] {
font-size: var(--canon-font-size-3);
padding: 0 var(--canon-space-2);
height: 32px;
height: 2rem;
}
.canon-ButtonIcon[data-size="small"], .canon-ButtonIcon[data-size="small"] svg {
.canon-Button[data-size="small"] svg {
width: 1rem;
height: 1rem;
}
.canon-ButtonIcon[data-size="medium"], .canon-ButtonIcon[data-size="medium"] svg {
.canon-Button[data-size="medium"] svg {
width: 1.25rem;
height: 1.25rem;
}
.canon-ButtonIcon {
justify-content: center;
align-items: center;
}
.canon-ButtonIcon[data-size="small"] {
width: 2rem;
padding: 0;
}
.canon-ButtonIcon[data-size="medium"] {
width: 2.5rem;
padding: 0;
}
.canon-CheckboxRoot {
width: 1rem;
height: 1rem;
@@ -9575,96 +9590,6 @@
height: 1rem;
}
.canon-IconButton {
user-select: none;
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-bold);
cursor: pointer;
border-radius: var(--canon-radius-2);
justify-content: center;
align-items: center;
gap: var(--canon-space-1_5);
border: none;
padding: 0;
display: inline-flex;
&:disabled {
cursor: not-allowed;
}
}
.canon-IconButton[data-variant="primary"] {
background-color: var(--canon-bg-solid);
color: var(--canon-fg-solid);
transition: background-color .15s, box-shadow .15s;
&:hover {
background-color: var(--canon-bg-solid-hover);
}
&:active {
background-color: var(--canon-bg-solid-pressed);
}
&:focus-visible {
outline: 2px solid var(--canon-ring);
outline-offset: 2px;
}
&:disabled {
background-color: var(--canon-bg-solid-disabled);
color: var(--canon-fg-solid-disabled);
}
}
.canon-IconButton[data-variant="secondary"] {
background-color: var(--canon-bg-surface-1);
box-shadow: inset 0 0 0 1px var(--canon-border);
color: var(--canon-fg-primary);
transition: box-shadow .15s;
&:hover {
box-shadow: inset 0 0 0 1px var(--canon-border-hover);
}
&:active {
box-shadow: inset 0 0 0 1px var(--canon-border-pressed);
}
&:focus-visible {
box-shadow: inset 0 0 0 2px var(--canon-ring);
outline: none;
transition: none;
}
&:disabled {
box-shadow: inset 0 0 0 1px var(--canon-border-disabled);
color: var(--canon-fg-disabled);
}
}
.canon-IconButton[data-size="medium"] {
font-size: var(--canon-font-size-4);
width: 40px;
height: 40px;
}
.canon-IconButton[data-size="small"] {
font-size: var(--canon-font-size-3);
width: 32px;
height: 32px;
}
.canon-IconButtonIcon[data-size="small"], .canon-IconButtonIcon[data-size="small"] svg {
width: 1rem;
height: 1rem;
}
.canon-IconButtonIcon[data-size="medium"], .canon-IconButtonIcon[data-size="medium"] svg {
width: 1.25rem;
height: 1.25rem;
}
.canon-Link {
font-family: var(--canon-font-regular);
color: var(--canon-fg-link);
+30 -57
View File
@@ -5,6 +5,7 @@
```ts
import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar';
import { Breakpoint as Breakpoint_2 } from '@backstage/canon';
import { ButtonProps as ButtonProps_2 } from 'react-aria-components';
import { ChangeEvent } from 'react';
import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible';
import { ComponentProps } from 'react';
@@ -152,36 +153,42 @@ export const breakpoints: Breakpoint[];
// @public (undocumented)
export const Button: ForwardRefExoticComponent<
Omit<ButtonProps, 'ref'> & RefAttributes<HTMLButtonElement>
ButtonProps & RefAttributes<HTMLButtonElement>
>;
// @public (undocumented)
export type ButtonOwnProps = GetPropDefTypes<typeof buttonPropDefs>;
// @public (undocumented)
export const buttonPropDefs: {
variant: {
type: 'enum';
values: ('primary' | 'secondary')[];
className: string;
default: 'primary';
responsive: true;
};
size: {
type: 'enum';
values: ('small' | 'medium')[];
className: string;
default: 'medium';
responsive: true;
};
};
export const ButtonIcon: ForwardRefExoticComponent<
ButtonIconProps & RefAttributes<HTMLButtonElement>
>;
// @public
export interface ButtonProps extends useRender.ComponentProps<'button'> {
export interface ButtonIconProps extends ButtonProps_2 {
// (undocumented)
icon?: ReactElement;
// (undocumented)
size?: 'small' | 'medium' | Partial<Record<Breakpoint_2, 'small' | 'medium'>>;
// (undocumented)
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint_2, 'primary' | 'secondary'>>;
}
// @public
export interface ButtonProps extends ButtonProps_2 {
// (undocumented)
children?: ReactNode;
// (undocumented)
iconEnd?: ReactElement;
// (undocumented)
iconStart?: ReactElement;
size?: ButtonOwnProps['size'];
variant?: ButtonOwnProps['variant'];
// (undocumented)
size?: 'small' | 'medium' | Partial<Record<Breakpoint_2, 'small' | 'medium'>>;
// (undocumented)
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint_2, 'primary' | 'secondary'>>;
}
// @public (undocumented)
@@ -691,40 +698,6 @@ export type HeightProps = GetPropDefTypes<typeof heightPropDefs>;
// @public (undocumented)
export const Icon: (props: IconProps) => JSX_2.Element | null;
// @public (undocumented)
export const IconButton: ForwardRefExoticComponent<
IconButtonProps & RefAttributes<HTMLButtonElement>
>;
// @public (undocumented)
export type IconButtonOwnProps = GetPropDefTypes<typeof iconButtonPropDefs>;
// @public (undocumented)
export const iconButtonPropDefs: {
variant: {
type: 'enum';
values: ('primary' | 'secondary')[];
className: string;
default: 'primary';
responsive: true;
};
size: {
type: 'enum';
values: ('small' | 'medium')[];
className: string;
default: 'medium';
responsive: true;
};
};
// @public
export interface IconButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
icon: ReactElement;
size?: IconButtonOwnProps['size'];
variant?: IconButtonOwnProps['variant'];
}
// @public (undocumented)
export const IconContext: Context<IconContextProps>;
@@ -1,41 +0,0 @@
/*
* Copyright 2025 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 type { PropDef, GetPropDefTypes } from '../../props/prop-def';
/** @public */
export const buttonPropDefs = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
className: 'canon-Button--variant',
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
className: 'canon-Button--size',
default: 'medium',
responsive: true,
},
} satisfies {
variant: PropDef<'primary' | 'secondary'>;
size: PropDef<'small' | 'medium'>;
};
/** @public */
export type ButtonOwnProps = GetPropDefTypes<typeof buttonPropDefs>;
@@ -18,7 +18,6 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { ButtonProps } from './types';
import { Icon } from '../Icon';
const meta = {
@@ -119,30 +118,18 @@ export const FullWidth: Story = {
};
export const Disabled: Story = {
args: {
children: 'Button',
disabled: true,
},
render: args => (
render: () => (
<Flex direction="row" gap="4">
<Button {...args} variant="primary" />
<Button {...args} variant="secondary" />
<Button variant="primary" isDisabled>
Primary
</Button>
<Button variant="secondary" isDisabled>
Secondary
</Button>
</Flex>
),
};
export const AsLink: Story = {
args: {
children: 'I am a link',
},
render: args => (
<Button
{...args}
render={<a href="https://canon.backstage.io" target="_blank" />}
/>
),
};
export const Responsive: Story = {
args: {
children: 'Button',
@@ -157,7 +144,8 @@ export const Responsive: Story = {
},
};
const variants: string[] = ['primary', 'secondary'];
const variants = ['primary', 'secondary'] as const;
const sizes = ['small', 'medium'] as const;
export const Playground: Story = {
args: {
@@ -168,25 +156,22 @@ export const Playground: Story = {
{variants.map(variant => (
<Flex direction="column" key={variant}>
<Text>{variant}</Text>
{['small', 'medium'].map(size => (
{sizes.map(size => (
<Flex align="center" key={size}>
<Button
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
>
<Button variant={variant} size={size}>
Button
</Button>
<Button
iconStart={<Icon name="cloud" />}
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
variant={variant}
size={size}
>
Button
</Button>
<Button
iconEnd={<Icon name="chevron-right" />}
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
variant={variant}
size={size}
>
Button
</Button>
@@ -194,31 +179,27 @@ export const Playground: Story = {
iconStart={<Icon name="cloud" />}
iconEnd={<Icon name="chevron-right" />}
style={{ width: '200px' }}
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
variant={variant}
size={size}
>
Button
</Button>
<Button
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
disabled
>
<Button variant={variant} size={size} isDisabled>
Button
</Button>
<Button
iconStart={<Icon name="cloud" />}
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
disabled
variant={variant}
size={size}
isDisabled
>
Button
</Button>
<Button
iconEnd={<Icon name="chevron-right" />}
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
disabled
variant={variant}
size={size}
isDisabled
>
Button
</Button>
+18 -45
View File
@@ -14,69 +14,42 @@
* limitations under the License.
*/
import { forwardRef, useRef } from 'react';
import clsx from 'clsx';
import { forwardRef, Ref } from 'react';
import { Button as RAButton } from 'react-aria-components';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import { useRender } from '@base-ui-components/react/use-render';
import type { ButtonProps } from './types';
/** @public */
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props: ButtonProps, ref) => {
export const Button = forwardRef(
(props: ButtonProps, ref: Ref<HTMLButtonElement>) => {
const {
size = 'small',
variant = 'primary',
iconStart,
iconEnd,
children,
render = <button />,
className,
style,
...rest
} = props;
// Get the responsive value for the variant
const responsiveSize = useResponsiveValue(size);
const responsiveVariant = useResponsiveValue(variant);
const internalRef = useRef<HTMLElement | null>(null);
const { renderElement } = useRender({
render,
props: {
className: clsx('canon-Button', className),
['data-variant']: responsiveVariant,
['data-size']: responsiveSize,
...rest,
children: (
<>
{iconStart && (
<span
className="canon-ButtonIcon"
aria-hidden="true"
data-size={responsiveSize}
>
{iconStart}
</span>
)}
{children}
{iconEnd && (
<span
className="canon-ButtonIcon"
aria-hidden="true"
data-size={responsiveSize}
>
{iconEnd}
</span>
)}
</>
),
},
refs: [ref, internalRef],
});
return renderElement();
return (
<RAButton
className={clsx('canon-Button', className)}
data-variant={responsiveVariant}
data-size={responsiveSize}
ref={ref}
{...rest}
>
{iconStart}
{children}
{iconEnd}
</RAButton>
);
},
);
export default Button;
Button.displayName = 'Button';
@@ -14,7 +14,5 @@
* limitations under the License.
*/
export { IconButton } from './IconButton';
export type { IconButtonProps } from './types';
export { iconButtonPropDefs } from './IconButton.props';
export type { IconButtonOwnProps } from './IconButton.props';
export * from './Button';
export * from './types';
@@ -85,23 +85,21 @@
.canon-Button[data-size='medium'] {
font-size: var(--canon-font-size-4);
padding: 0 var(--canon-space-3);
height: 40px;
height: 2.5rem;
}
.canon-Button[data-size='small'] {
font-size: var(--canon-font-size-3);
padding: 0 var(--canon-space-2);
height: 32px;
height: 2rem;
}
.canon-ButtonIcon[data-size='small'],
.canon-ButtonIcon[data-size='small'] svg {
.canon-Button[data-size='small'] svg {
width: 1rem;
height: 1rem;
}
.canon-ButtonIcon[data-size='medium'],
.canon-ButtonIcon[data-size='medium'] svg {
.canon-Button[data-size='medium'] svg {
width: 1.25rem;
height: 1.25rem;
}
+10 -23
View File
@@ -14,35 +14,22 @@
* limitations under the License.
*/
import type { ButtonOwnProps } from './Button.props';
import { ReactElement } from 'react';
import type { useRender } from '@base-ui-components/react/use-render';
import { Breakpoint } from '@backstage/canon';
import { ReactElement, ReactNode } from 'react';
import { ButtonProps as RAButtonProps } from 'react-aria-components';
/**
* Properties for {@link Button}
*
* @public
*/
export interface ButtonProps extends useRender.ComponentProps<'button'> {
/**
* The size of the button
* @defaultValue 'medium'
*/
size?: ButtonOwnProps['size'];
/**
* The visual variant of the button
* @defaultValue 'primary'
*/
variant?: ButtonOwnProps['variant'];
/**
* Optional icon to display at the start of the button
*/
export interface ButtonProps extends RAButtonProps {
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint, 'primary' | 'secondary'>>;
iconStart?: ReactElement;
/**
* Optional icon to display at the end of the button
*/
iconEnd?: ReactElement;
children?: ReactNode;
}
@@ -15,15 +15,14 @@
*/
import type { Meta, StoryObj } from '@storybook/react';
import { IconButton } from './IconButton';
import { ButtonIcon } from './ButtonIcon';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { IconButtonProps } from './types';
import { Icon } from '../Icon';
const meta = {
title: 'Components/IconButton',
component: IconButton,
title: 'Components/ButtonIcon',
component: ButtonIcon,
argTypes: {
size: {
control: 'select',
@@ -34,59 +33,44 @@ const meta = {
options: ['primary', 'secondary'],
},
},
} satisfies Meta<typeof IconButton>;
} satisfies Meta<typeof ButtonIcon>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
icon: <Icon name="cloud" />,
},
render: () => <ButtonIcon icon={<Icon name="cloud" />} />,
};
export const Variants: Story = {
args: {
...Default.args,
},
render: args => (
render: () => (
<Flex align="center" gap="2">
<IconButton {...args} variant="primary" />
<IconButton {...args} variant="secondary" />
<ButtonIcon icon={<Icon name="cloud" />} variant="primary" />
<ButtonIcon icon={<Icon name="cloud" />} variant="secondary" />
</Flex>
),
};
export const Sizes: Story = {
args: {
icon: <Icon name="cloud" />,
},
render: args => (
render: () => (
<Flex align="center" gap="2">
<IconButton {...args} size="small" />
<IconButton {...args} size="medium" />
<ButtonIcon icon={<Icon name="cloud" />} size="small" />
<ButtonIcon icon={<Icon name="cloud" />} size="medium" />
</Flex>
),
};
export const Disabled: Story = {
args: {
icon: <Icon name="cloud" />,
disabled: true,
'aria-label': 'Cloud icon button',
},
render: args => (
render: () => (
<Flex direction="row" gap="2">
<IconButton {...args} variant="primary" />
<IconButton {...args} variant="secondary" />
<ButtonIcon isDisabled icon={<Icon name="cloud" />} variant="primary" />
<ButtonIcon isDisabled icon={<Icon name="cloud" />} variant="secondary" />
</Flex>
),
};
export const Responsive: Story = {
args: {
icon: <Icon name="cloud" />,
'aria-label': 'Cloud icon button',
variant: {
initial: 'primary',
sm: 'secondary',
@@ -96,40 +80,39 @@ export const Responsive: Story = {
sm: 'medium',
},
},
render: args => <ButtonIcon {...args} icon={<Icon name="cloud" />} />,
};
const variants: string[] = ['primary', 'secondary'];
const variants = ['primary', 'secondary'] as const;
const sizes = ['small', 'medium'] as const;
export const Playground: Story = {
args: {
icon: <Icon name="cloud" />,
'aria-label': 'Cloud icon button',
},
render: args => (
<Flex direction="column">
{variants.map(variant => (
<Flex direction="column" key={variant}>
<Text>{variant}</Text>
{['small', 'medium'].map(size => (
{sizes.map(size => (
<Flex align="center" key={size}>
<IconButton
<ButtonIcon
{...args}
variant={variant as IconButtonProps['variant']}
size={size as IconButtonProps['size']}
variant={variant}
size={size}
icon={<Icon name="cloud" />}
/>
<IconButton
<ButtonIcon
{...args}
icon={<Icon name="chevron-right" />}
aria-label="Chevron right icon button"
variant={variant as IconButtonProps['variant']}
size={size as IconButtonProps['size']}
variant={variant}
size={size}
/>
<IconButton
<ButtonIcon
{...args}
icon={<Icon name="chevron-right" />}
aria-label="Chevron right icon button"
variant={variant as IconButtonProps['variant']}
size={size as IconButtonProps['size']}
variant={variant}
size={size}
/>
</Flex>
))}
@@ -14,15 +14,15 @@
* limitations under the License.
*/
import { forwardRef } from 'react';
import clsx from 'clsx';
import { forwardRef, Ref } from 'react';
import { Button as RAButton } from 'react-aria-components';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import type { IconButtonProps } from './types';
import type { ButtonIconProps } from './types';
/** @public */
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
(props: IconButtonProps, ref) => {
export const ButtonIcon = forwardRef(
(props: ButtonIconProps, ref: Ref<HTMLButtonElement>) => {
const {
size = 'small',
variant = 'primary',
@@ -36,24 +36,17 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
const responsiveVariant = useResponsiveValue(variant);
return (
<button
ref={ref}
className={clsx('canon-IconButton', className)}
data-size={responsiveSize}
<RAButton
className={clsx('canon-Button', 'canon-ButtonIcon', className)}
data-variant={responsiveVariant}
style={style}
data-size={responsiveSize}
ref={ref}
{...rest}
>
<span
className="canon-IconButtonIcon"
aria-hidden="true"
data-size={responsiveSize}
>
{icon}
</span>
</button>
{icon}
</RAButton>
);
},
);
export default IconButton;
ButtonIcon.displayName = 'ButtonIcon';
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Button } from './Button';
export type { ButtonProps } from './types';
export { buttonPropDefs } from './Button.props';
export type { ButtonOwnProps } from './Button.props';
export * from './ButtonIcon';
export * from './types';
@@ -0,0 +1,30 @@
/*
* 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.
*/
.canon-ButtonIcon {
align-items: center;
justify-content: center;
}
.canon-ButtonIcon[data-size='small'] {
padding: 0;
width: 2rem;
}
.canon-ButtonIcon[data-size='medium'] {
padding: 0;
width: 2.5rem;
}
@@ -14,30 +14,20 @@
* limitations under the License.
*/
import type { IconButtonOwnProps } from './IconButton.props';
import { Breakpoint } from '@backstage/canon';
import { ReactElement } from 'react';
import { ButtonProps as RAButtonProps } from 'react-aria-components';
/**
* Properties for {@link IconButton}
* Properties for {@link ButtonIcon}
*
* @public
*/
export interface IconButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
/**
* The size of the button
* @defaultValue 'medium'
*/
size?: IconButtonOwnProps['size'];
/**
* The visual variant of the button
* @defaultValue 'primary'
*/
variant?: IconButtonOwnProps['variant'];
/**
* Icon to display in the button
*/
icon: ReactElement;
export interface ButtonIconProps extends RAButtonProps {
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint, 'primary' | 'secondary'>>;
icon?: ReactElement;
}
@@ -0,0 +1,212 @@
/*
* 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 type { Meta, StoryObj } from '@storybook/react';
import { ButtonLink } from './ButtonLink';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { Icon } from '../Icon';
const meta = {
title: 'Components/ButtonLink',
component: ButtonLink,
argTypes: {
size: {
control: 'select',
options: ['small', 'medium'],
},
variant: {
control: 'select',
options: ['primary', 'secondary'],
},
},
} satisfies Meta<typeof ButtonLink>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Button',
},
};
export const Variants: Story = {
render: () => (
<Flex align="center">
<ButtonLink
iconStart={<Icon name="cloud" />}
variant="primary"
href="https://canon.backstage.io"
target="_blank"
>
Button
</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
variant="secondary"
href="https://canon.backstage.io"
target="_blank"
>
Button
</ButtonLink>
</Flex>
),
};
export const Sizes: Story = {
args: {
children: 'Button',
},
render: () => (
<Flex align="center">
<ButtonLink size="small" iconStart={<Icon name="cloud" />}>
Small
</ButtonLink>
<ButtonLink size="medium" iconStart={<Icon name="cloud" />}>
Medium
</ButtonLink>
</Flex>
),
};
export const WithIcons: Story = {
args: {
children: 'Button',
},
render: args => (
<Flex align="center">
<ButtonLink {...args} iconStart={<Icon name="cloud" />} />
<ButtonLink {...args} iconEnd={<Icon name="chevron-right" />} />
<ButtonLink
{...args}
iconStart={<Icon name="cloud" />}
iconEnd={<Icon name="chevron-right" />}
/>
</Flex>
),
};
export const FullWidth: Story = {
args: {
children: 'Button',
},
render: args => (
<Flex direction="column" gap="4" style={{ width: '300px' }}>
<ButtonLink {...args} iconStart={<Icon name="cloud" />} />
<ButtonLink {...args} iconEnd={<Icon name="chevron-right" />} />
<ButtonLink
{...args}
iconStart={<Icon name="cloud" />}
iconEnd={<Icon name="chevron-right" />}
/>
</Flex>
),
};
export const Disabled: Story = {
render: () => (
<Flex direction="row" gap="4">
<ButtonLink variant="primary" isDisabled>
Primary
</ButtonLink>
<ButtonLink variant="secondary" isDisabled>
Secondary
</ButtonLink>
</Flex>
),
};
export const Responsive: Story = {
args: {
children: 'Button',
variant: {
initial: 'primary',
sm: 'secondary',
},
size: {
xs: 'small',
sm: 'medium',
},
},
};
const variants = ['primary', 'secondary'] as const;
const sizes = ['small', 'medium'] as const;
export const Playground: Story = {
args: {
children: 'Button',
},
render: () => (
<Flex direction="column">
{variants.map(variant => (
<Flex direction="column" key={variant}>
<Text>{variant}</Text>
{sizes.map(size => (
<Flex align="center" key={size}>
<ButtonLink variant={variant} size={size}>
Button
</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
variant={variant}
size={size}
>
Button
</ButtonLink>
<ButtonLink
iconEnd={<Icon name="chevron-right" />}
variant={variant}
size={size}
>
Button
</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
iconEnd={<Icon name="chevron-right" />}
style={{ width: '200px' }}
variant={variant}
size={size}
>
Button
</ButtonLink>
<ButtonLink variant={variant} size={size} isDisabled>
Button
</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
variant={variant}
size={size}
isDisabled
>
Button
</ButtonLink>
<ButtonLink
iconEnd={<Icon name="chevron-right" />}
variant={variant}
size={size}
isDisabled
>
Button
</ButtonLink>
</Flex>
))}
</Flex>
))}
</Flex>
),
};
@@ -0,0 +1,55 @@
/*
* 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 clsx from 'clsx';
import { forwardRef, Ref } from 'react';
import { Link as RALink } from 'react-aria-components';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import type { ButtonLinkProps } from './types';
/** @public */
export const ButtonLink = forwardRef(
(props: ButtonLinkProps, ref: Ref<HTMLAnchorElement>) => {
const {
size = 'small',
variant = 'primary',
iconStart,
iconEnd,
children,
className,
...rest
} = props;
const responsiveSize = useResponsiveValue(size);
const responsiveVariant = useResponsiveValue(variant);
return (
<RALink
className={clsx('canon-Button', 'canon-ButtonLink', className)}
data-variant={responsiveVariant}
data-size={responsiveSize}
ref={ref}
{...rest}
>
{iconStart}
{children}
{iconEnd}
</RALink>
);
},
);
ButtonLink.displayName = 'ButtonLink';
@@ -0,0 +1,18 @@
/*
* 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.
*/
export * from './ButtonLink';
export * from './types';
@@ -0,0 +1,35 @@
/*
* 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 { Breakpoint } from '@backstage/canon';
import { ReactElement, ReactNode } from 'react';
import { LinkProps as RALinkProps } from 'react-aria-components';
/**
* Properties for {@link ButtonLink}
*
* @public
*/
export interface ButtonLinkProps extends RALinkProps {
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
variant?:
| 'primary'
| 'secondary'
| Partial<Record<Breakpoint, 'primary' | 'secondary'>>;
iconStart?: ReactElement;
iconEnd?: ReactElement;
children?: ReactNode;
}
@@ -17,7 +17,7 @@
import { forwardRef } from 'react';
import { Text } from '../../Text';
import { DataTablePaginationProps } from './types';
import { IconButton } from '../../IconButton';
import { ButtonIcon } from '../../ButtonIcon';
import clsx from 'clsx';
import { Select } from '../../Select';
import { useDataTable } from '../Root/DataTableRoot';
@@ -71,18 +71,18 @@ const DataTablePagination = forwardRef(
</div>
<div className="canon-DataTablePagination--right">
<Text variant="body">{`${fromCount} - ${toCount} of ${rowCount}`}</Text>
<IconButton
<ButtonIcon
variant="secondary"
size="small"
onClick={() => table?.previousPage()}
disabled={!table?.getCanPreviousPage()}
isDisabled={!table?.getCanPreviousPage()}
icon={<Icon name="chevron-left" />}
/>
<IconButton
<ButtonIcon
variant="secondary"
size="small"
onClick={() => table?.nextPage()}
disabled={!table?.getCanNextPage()}
isDisabled={!table?.getCanNextPage()}
icon={<Icon name="chevron-right" />}
/>
</div>
@@ -1,41 +0,0 @@
/*
* Copyright 2025 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 type { PropDef, GetPropDefTypes } from '../../props/prop-def';
/** @public */
export const iconButtonPropDefs = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
className: 'canon-Button--variant',
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
className: 'canon-Button--size',
default: 'medium',
responsive: true,
},
} satisfies {
variant: PropDef<'primary' | 'secondary'>;
size: PropDef<'small' | 'medium'>;
};
/** @public */
export type IconButtonOwnProps = GetPropDefTypes<typeof iconButtonPropDefs>;
@@ -1,107 +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.
*/
.canon-IconButton {
border: none;
display: inline-flex;
align-items: center;
justify-content: center;
user-select: none;
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-bold);
padding: 0;
cursor: pointer;
border-radius: var(--canon-radius-2);
gap: var(--canon-space-1_5);
&:disabled {
cursor: not-allowed;
}
}
.canon-IconButton[data-variant='primary'] {
background-color: var(--canon-bg-solid);
color: var(--canon-fg-solid);
transition: background-color 150ms ease, box-shadow 150ms ease;
&:hover {
background-color: var(--canon-bg-solid-hover);
}
&:active {
background-color: var(--canon-bg-solid-pressed);
}
&:focus-visible {
outline: 2px solid var(--canon-ring);
outline-offset: 2px;
}
&:disabled {
background-color: var(--canon-bg-solid-disabled);
color: var(--canon-fg-solid-disabled);
}
}
.canon-IconButton[data-variant='secondary'] {
background-color: var(--canon-bg-surface-1);
box-shadow: inset 0 0 0 1px var(--canon-border);
color: var(--canon-fg-primary);
transition: box-shadow 150ms ease;
&:hover {
box-shadow: inset 0 0 0 1px var(--canon-border-hover);
}
&:active {
box-shadow: inset 0 0 0 1px var(--canon-border-pressed);
}
&:focus-visible {
outline: none;
transition: none;
box-shadow: inset 0 0 0 2px var(--canon-ring);
}
&:disabled {
box-shadow: inset 0 0 0 1px var(--canon-border-disabled);
color: var(--canon-fg-disabled);
}
}
.canon-IconButton[data-size='medium'] {
font-size: var(--canon-font-size-4);
height: 40px;
width: 40px;
}
.canon-IconButton[data-size='small'] {
font-size: var(--canon-font-size-3);
height: 32px;
width: 32px;
}
.canon-IconButtonIcon[data-size='small'],
.canon-IconButtonIcon[data-size='small'] svg {
width: 1rem;
height: 1rem;
}
.canon-IconButtonIcon[data-size='medium'],
.canon-IconButtonIcon[data-size='medium'] svg {
width: 1.25rem;
height: 1.25rem;
}
+1 -1
View File
@@ -17,6 +17,7 @@
@import '../components/Avatar/Avatar.styles.css';
@import '../components/Box/styles.css';
@import '../components/Button/styles.css';
@import '../components/ButtonIcon/styles.css';
@import '../components/Checkbox/styles.css';
@import '../components/Collapsible/Collapsible.styles.css';
@import '../components/Container/styles.css';
@@ -27,7 +28,6 @@
@import '../components/Grid/styles.css';
@import '../components/Heading/styles.css';
@import '../components/Icon/styles.css';
@import '../components/IconButton/styles.css';
@import '../components/Link/styles.css';
@import '../components/Menu/Menu.styles.css';
@import '../components/Table/styles.css';
+1 -1
View File
@@ -38,7 +38,7 @@ export * from './components/Collapsible';
export * from './components/DataTable';
export * from './components/FieldLabel';
export * from './components/Icon';
export * from './components/IconButton';
export * from './components/ButtonIcon';
export * from './components/Checkbox';
export * from './components/Table';
export * from './components/Tabs';
@@ -103,9 +103,6 @@ catalog:
# target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml
# rules:
# - allow: [User, Group]
# Experimental: Always use the search method in UrlReaderProcessor.
# New adopters are encouraged to enable it as this behavior will be the default in a future release.
useUrlReadersSearch: true
kubernetes:
# see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options
-14
View File
@@ -218,19 +218,5 @@ export interface Config {
* housing catalog-info files.
*/
processingInterval?: HumanDuration | false;
/**
* Defines if the UrlReaderProcessor should always call the search method of the
* different UrlReaders.
*
* If set to false, the UrlReaderProcessor will use the legacy behavior that tries to
* parse a Git URL and calls search if there's wildcard patterns and readUrl otherwise.
*
* If set to true, the UrlReaderProcessor always call the search method and lets each UrlReader
* determine if it's a search pattern or not.
*
* This flag is temporary and will be enabled by default in future releases.
*/
useUrlReadersSearch?: boolean;
};
}
-1
View File
@@ -99,7 +99,6 @@
"@backstage/cli": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/repo-tools": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@types/core-js": "^2.5.4",
"@types/express": "^4.17.6",
"@types/git-url-parse": "^9.0.0",
+1 -5
View File
@@ -135,11 +135,7 @@ export function transformLegacyPolicyToProcessor(
// @public (undocumented)
export class UrlReaderProcessor implements CatalogProcessor {
constructor(options: {
reader: UrlReaderService;
logger: LoggerService;
config?: Config;
});
constructor(options: { reader: UrlReaderService; logger: LoggerService });
// (undocumented)
getProcessorName(): string;
// (undocumented)
@@ -30,8 +30,6 @@ import {
import { defaultEntityDataParser } from '../util/parse';
import { UrlReaderProcessor } from './UrlReaderProcessor';
import { UrlReaders } from '@backstage/backend-defaults/urlReader';
import { UrlReaderService } from '@backstage/backend-plugin-api';
import { mockApis } from '@backstage/test-utils';
describe('UrlReaderProcessor', () => {
const mockApiOrigin = 'http://localhost';
@@ -191,11 +189,11 @@ describe('UrlReaderProcessor', () => {
expect(generated.location).toBe(spec);
expect(generated.error.name).toBe('NotFoundError');
expect(generated.error.message).toBe(
`Unable to read url, NotFoundError: could not read ${mockApiOrigin}/component-notfound.yaml, 404 Not Found`,
`Unable to read url, no matching files found for ${mockApiOrigin}/component-notfound.yaml`,
);
});
it('uses search when there are globs', async () => {
it("uses reader' search method", async () => {
const logger = mockServices.logger.mock();
const reader = mockServices.urlReader.mock({
@@ -206,38 +204,6 @@ describe('UrlReaderProcessor', () => {
const emit = jest.fn();
await processor.readLocation(
{ type: 'url', target: 'https://github.com/a/b/blob/x/**/b.yaml' },
false,
emit,
defaultEntityDataParser,
mockCache,
);
expect(reader.search).toHaveBeenCalledTimes(1);
});
it('uses search when catalog.useUrlReadersSearch flag is set to true', async () => {
const logger = mockServices.logger.mock();
const reader: jest.Mocked<UrlReaderService> = {
readUrl: jest.fn(),
readTree: jest.fn(),
search: jest.fn().mockImplementation(async () => []),
};
const config = mockApis.config({
data: {
catalog: {
useUrlReadersSearch: true,
},
},
});
const processor = new UrlReaderProcessor({ reader, logger, config });
const emit = jest.fn();
await processor.readLocation(
{ type: 'url', target: 'https://github.com/a/b/blob/x/b.yaml' },
false,
@@ -18,7 +18,6 @@ import { Entity } from '@backstage/catalog-model';
import { assertError } from '@backstage/errors';
import limiterFactory, { Limit } from 'p-limit';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import parseGitUrl from 'git-url-parse';
import {
CatalogProcessor,
CatalogProcessorCache,
@@ -29,7 +28,6 @@ import {
processingResult,
} from '@backstage/plugin-catalog-node';
import { LoggerService, UrlReaderService } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
const CACHE_KEY = 'v1';
@@ -48,25 +46,14 @@ export class UrlReaderProcessor implements CatalogProcessor {
// This limiter is used for only consuming a limited number of read streams
// concurrently.
#limiter: Limit;
#useUrlReadersSearch: boolean;
constructor(
private readonly options: {
reader: UrlReaderService;
logger: LoggerService;
config?: Config;
},
) {
this.#limiter = limiterFactory(5);
this.#useUrlReadersSearch =
this.options.config?.getOptionalBoolean('catalog.useUrlReadersSearch') ||
false;
if (!this.#useUrlReadersSearch) {
this.options.logger.warn(
'UrlReaderProcessor uses the legacy readUrl/search behavior which will be removed in a future release. Set catalog.useUrlReadersSearch to true to adopt the new behavior.',
);
}
}
getProcessorName() {
@@ -149,35 +136,13 @@ export class UrlReaderProcessor implements CatalogProcessor {
location: string,
etag?: string,
): Promise<{ response: { data: Buffer; url: string }[]; etag?: string }> {
// New behavior: always use the search method
if (this.#useUrlReadersSearch) {
const response = await this.options.reader.search(location, { etag });
const response = await this.options.reader.search(location, { etag });
const output = response.files.map(async file => ({
url: file.url,
data: await this.#limiter(file.content),
}));
const output = response.files.map(async file => ({
url: file.url,
data: await this.#limiter(file.content),
}));
return { response: await Promise.all(output), etag: response.etag };
}
// Old behavior: Does it contain globs? I.e. does it contain asterisks or question marks
// (no curly braces for now)
const { filepath } = parseGitUrl(location);
if (filepath?.match(/[*?]/)) {
const response = await this.options.reader.search(location, { etag });
const output = response.files.map(async file => ({
url: file.url,
data: await this.#limiter(file.content),
}));
return { response: await Promise.all(output), etag: response.etag };
}
const data = await this.options.reader.readUrl(location, { etag });
return {
response: [{ url: location, data: await data.buffer() }],
etag: data.etag,
};
return { response: await Promise.all(output), etag: response.etag };
}
}
@@ -356,7 +356,7 @@ export class CatalogBuilder {
return [
new FileReaderProcessor(),
new UrlReaderProcessor({ reader, logger, config }),
new UrlReaderProcessor({ reader, logger }),
new AnnotateLocationEntityProcessor({ integrations }),
];
}
+1
View File
@@ -89,6 +89,7 @@ backend:
- type: static
options:
token: ${MCP_TOKEN}
subject: mcp-clients
accessRestrictions:
- plugin: mcp-actions
- plugin: catalog
@@ -4,10 +4,12 @@
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { objectOutputType } from 'zod';
import { ScmIntegrations } from '@backstage/integration';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { UrlReaderService } from '@backstage/backend-plugin-api';
import { Writable } from 'stream';
import { ZodTypeAny } from 'zod';
// @public
export interface ContainerRunner {
@@ -47,7 +49,7 @@ export function createFetchCookiecutterAction(options: {
}): TemplateAction<
{
url: string;
values: Record<string, unknown>;
values: objectOutputType<{}, ZodTypeAny, 'passthrough'>;
targetPath?: string | undefined;
copyWithoutRender?: string[] | undefined;
extensions?: string[] | undefined;
@@ -169,9 +169,14 @@ export function createFetchCookiecutterAction(options: {
})
.optional(),
values: z =>
z.record(z.unknown(), {
description: 'Values to pass on to cookiecutter for templating',
}),
z
.object(
{},
{
description: 'Values to pass on to cookiecutter for templating',
},
)
.passthrough(),
copyWithoutRender: z =>
z
.array(z.string(), {
@@ -0,0 +1,72 @@
/*
* Copyright 2025 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 { render } from '@testing-library/react';
// We need to mock react-router-dom hooks used by useInitialRedirect
import { useLocation, useNavigate, useParams } from 'react-router-dom';
// Import the module from which the hook is defined
import { useInitialRedirect } from './dom';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
useNavigate: jest.fn(),
useParams: jest.fn(),
}));
describe('useInitialRedirect', () => {
const mockNavigate = jest.fn();
beforeEach(() => {
// Reset mocks before each test
mockNavigate.mockReset();
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
(useLocation as jest.Mock).mockReturnValue({
pathname: '/docs/default/Component/backstage-demo',
});
// Simulate that no current path is provided
(useParams as jest.Mock).mockReturnValue({ '*': '' });
});
const TestComponent: React.FC<{ defaultPath?: string }> = ({
defaultPath,
}) => {
// Call hook that should trigger a redirect on mount only if defaultPath is a non-empty string.
useInitialRedirect(defaultPath);
return <div>Test</div>;
};
it('should not navigate when defaultPath is undefined', () => {
render(<TestComponent defaultPath={undefined} />);
expect(mockNavigate).not.toHaveBeenCalled();
});
it('should navigate when defaultPath is a non-empty string', () => {
render(<TestComponent defaultPath="/overview" />);
expect(mockNavigate).toHaveBeenCalledWith(
'/docs/default/Component/backstage-demo/overview',
{ replace: true },
);
});
it('should not navigate if currPath is non-empty', () => {
// Override useParams to simulate a non-empty currPath
(useParams as jest.Mock).mockReturnValue({ '*': 'existing-path' });
render(<TestComponent defaultPath={undefined} />);
expect(mockNavigate).not.toHaveBeenCalled();
});
});
@@ -61,15 +61,13 @@ const MOBILE_MEDIA_QUERY = 'screen and (max-width: 76.1875em)';
// If a defaultPath is specified then we should navigate to that path replacing the
// current location in the history. This should only happen on the initial load so
// navigating to the root of the docs doesn't also redirect.
const useInitialRedirect = (defaultPath?: string) => {
// const hasRun = useRef(false);
export const useInitialRedirect = (defaultPath?: string) => {
const location = useLocation();
const navigate = useNavigate();
const { '*': currPath = '' } = useParams();
useLayoutEffect(() => {
if (currPath === '' && defaultPath !== '') {
if (currPath === '' && defaultPath) {
navigate(`${location.pathname}${defaultPath}`, { replace: true });
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
-1
View File
@@ -5864,7 +5864,6 @@ __metadata:
"@backstage/plugin-permission-common": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
"@backstage/repo-tools": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
"@opentelemetry/api": "npm:^1.9.0"
"@types/core-js": "npm:^2.5.4"