diff --git a/.changeset/chubby-dots-kiss.md b/.changeset/chubby-dots-kiss.md new file mode 100644 index 0000000000..7a28467725 --- /dev/null +++ b/.changeset/chubby-dots-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fixed an issue causing TechDocs to not properly handle initial redirect. diff --git a/.changeset/cyan-islands-swim.md b/.changeset/cyan-islands-swim.md new file mode 100644 index 0000000000..396cc6fc68 --- /dev/null +++ b/.changeset/cyan-islands-swim.md @@ -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. diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..60eb6ca6ce --- /dev/null +++ b/.changeset/pre.json @@ -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": [] +} diff --git a/.changeset/public-nails-melt.md b/.changeset/public-nails-melt.md new file mode 100644 index 0000000000..1529ee94d0 --- /dev/null +++ b/.changeset/public-nails-melt.md @@ -0,0 +1,13 @@ +--- +'@backstage/canon': minor +--- + +**BREAKING CHANGES** + +We’re updating our Button component to provide better support for button links. + +- We’re 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. diff --git a/.changeset/rich-eagles-rule.md b/.changeset/rich-eagles-rule.md new file mode 100644 index 0000000000..100ccafb77 --- /dev/null +++ b/.changeset/rich-eagles-rule.md @@ -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 diff --git a/.changeset/sixty-ties-matter.md b/.changeset/sixty-ties-matter.md new file mode 100644 index 0000000000..3be0bb08a8 --- /dev/null +++ b/.changeset/sixty-ties-matter.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +--- + +Fixing the typescript issue with using `z.unknown()` diff --git a/app-config.yaml b/app-config.yaml index 89926357c2..910a3e4f99 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -138,7 +138,6 @@ integrations: secretAccessKey: ${AWS_SECRET_ACCESS_KEY} catalog: - useUrlReadersSearch: true import: entityFilename: catalog-info.yaml pullRequestBranchName: backstage-integration diff --git a/canon-docs/src/components/Sidebar/Sidebar.module.css b/canon-docs/src/components/Sidebar/Sidebar.module.css index e558621670..4eb49682df 100644 --- a/canon-docs/src/components/Sidebar/Sidebar.module.css +++ b/canon-docs/src/components/Sidebar/Sidebar.module.css @@ -55,10 +55,6 @@ gap: 2px; } -.playgroundSection { - position: absolute; -} - .sectionTitle { font-size: var(--canon-font-size-3); font-weight: var(--canon-font-weight-bold); diff --git a/canon-docs/src/components/Sidebar/playground.tsx b/canon-docs/src/components/Sidebar/playground.tsx index eb1c3213cc..2aaa4fe2c6 100644 --- a/canon-docs/src/components/Sidebar/playground.tsx +++ b/canon-docs/src/components/Sidebar/playground.tsx @@ -38,9 +38,7 @@ export const Playground = () => { return ( { visibility: isPlayground ? 'visible' : 'hidden', }} transition={{ duration: 0.2 }} + style={{ position: 'absolute' }} >
Components
{components.map(({ slug, title }) => ( diff --git a/canon-docs/src/content/components/button-icon.mdx b/canon-docs/src/content/components/button-icon.mdx new file mode 100644 index 0000000000..b020b19f33 --- /dev/null +++ b/canon-docs/src/content/components/button-icon.mdx @@ -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. + +} + code={buttonIconDefaultSnippet} +/> + + + +## API reference + + + +## Examples + +### Variants + +Here's a view when buttons have different variants. + +} + code={buttonIconVariantsSnippet} +/> + +### Sizes + +Here's a view when buttons have different sizes. + +} + code={buttonIconSizesSnippet} +/> + +### Disabled + +Here's a view when buttons are disabled. + +} + code={buttonIconDisabledSnippet} +/> + +### Responsive + +Here's a view when buttons are responsive. + + diff --git a/canon-docs/src/content/components/button-icon.props.ts b/canon-docs/src/content/components/button-icon.props.ts new file mode 100644 index 0000000000..a21bf3cd57 --- /dev/null +++ b/canon-docs/src/content/components/button-icon.props.ts @@ -0,0 +1,59 @@ +import { + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const buttonIconPropDefs: Record = { + 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'; + +`; + +export const buttonIconDefaultSnippet = ` + } variant="primary" /> + } variant="secondary" /> +`; + +export const buttonIconVariantsSnippet = ` + } variant="primary" /> + } variant="secondary" /> +`; + +export const buttonIconSizesSnippet = ` + } size="small" /> + } size="medium" /> +`; + +export const buttonIconDisabledSnippet = `} isDisabled />`; + +export const buttonIconResponsiveSnippet = `} variant={{ initial: 'primary', lg: 'secondary' }} />`; + +export const buttonIconAsLinkSnippet = `import { ButtonLink } from '@backstage/canon'; + + + Button +`; diff --git a/canon-docs/src/content/components/button-link.mdx b/canon-docs/src/content/components/button-link.mdx new file mode 100644 index 0000000000..f6da987941 --- /dev/null +++ b/canon-docs/src/content/components/button-link.mdx @@ -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. + +} + code={buttonLinkVariantsSnippet} +/> + + + +## API reference + + + +## Examples + +### Variants + +Here's a view when buttons have different variants. + +} + code={buttonLinkVariantsSnippet} +/> + +### Sizes + +Here's a view when buttons have different sizes. + +} + code={buttonLinkSizesSnippet} +/> + +### With Icons + +Here's a view when buttons have icons. + +} + code={buttonLinkIconsSnippet} +/> + +### Disabled + +Here's a view when buttons are disabled. + +} + code={buttonLinkDisabledSnippet} +/> + +### Responsive + +Here's a view when buttons are responsive. + + diff --git a/canon-docs/src/content/components/button-link.props.ts b/canon-docs/src/content/components/button-link.props.ts new file mode 100644 index 0000000000..b92b3154e5 --- /dev/null +++ b/canon-docs/src/content/components/button-link.props.ts @@ -0,0 +1,73 @@ +import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs'; +import type { PropDef } from '../../utils/propDefs'; + +export const buttonLinkPropDefs: Record = { + 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'; + +`; + +export const buttonLinkVariantsSnippet = ` + } variant="primary"> + Button + + } variant="secondary"> + Button + +`; + +export const buttonLinkSizesSnippet = ` + Small + Medium +`; + +export const buttonLinkIconsSnippet = ` + }>Button + }>Button + } + iconEnd={}> + Button + +`; + +export const buttonLinkDisabledSnippet = ` + + Primary + + + Secondary + +`; + +export const buttonLinkResponsiveSnippet = ` + Responsive Button +`; diff --git a/canon-docs/src/content/components/button.mdx b/canon-docs/src/content/components/button.mdx index aff74fe60b..c0053d0f5e 100644 --- a/canon-docs/src/content/components/button.mdx +++ b/canon-docs/src/content/components/button.mdx @@ -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. @@ -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. - -} - 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. + +### As Link + +If you want to use a button as a link, please use the `ButtonLink` component. + +} + code={buttonAsLinkSnippet} +/> diff --git a/canon-docs/src/content/components/button.props.ts b/canon-docs/src/content/components/button.props.ts index fc833a5f73..b32cba4725 100644 --- a/canon-docs/src/content/components/button.props.ts +++ b/canon-docs/src/content/components/button.props.ts @@ -14,6 +14,16 @@ export const buttonPropDefs: Record = { 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 = ` - `; export const buttonSizesSnippet = ` @@ -40,20 +47,16 @@ export const buttonSizesSnippet = ` `; export const buttonIconsSnippet = ` - - - -`; - -export const buttonFullWidthSnippet = ` - + + + `; export const buttonDisabledSnippet = ` - - `; @@ -61,3 +64,9 @@ export const buttonDisabledSnippet = ` export const buttonResponsiveSnippet = ``; + +export const buttonAsLinkSnippet = `import { ButtonLink } from '@backstage/canon'; + + + Button +`; diff --git a/canon-docs/src/content/components/icon-button.mdx b/canon-docs/src/content/components/icon-button.mdx deleted file mode 100644 index 80e7e5a46d..0000000000 --- a/canon-docs/src/content/components/icon-button.mdx +++ /dev/null @@ -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. - -} - code={iconButtonDefaultSnippet} -/> - - - -## API reference - - - -## Examples - -### Variants - -Here's a view when buttons have different variants. - -} - code={iconButtonVariantsSnippet} -/> - -### Sizes - -Here's a view when buttons have different sizes. - -} - code={iconButtonSizesSnippet} -/> - -### Disabled - -Here's a view when buttons are disabled. - -} - code={iconButtonDisabledSnippet} -/> - -### Responsive - -Here's a view when buttons are responsive. - - diff --git a/canon-docs/src/content/components/icon-button.props.ts b/canon-docs/src/content/components/icon-button.props.ts deleted file mode 100644 index 8fdfaba6b9..0000000000 --- a/canon-docs/src/content/components/icon-button.props.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const iconButtonPropDefs: Record = { - 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'; - -`; - -export const iconButtonDefaultSnippet = ` - - -`; - -export const iconButtonVariantsSnippet = ` - - -`; - -export const iconButtonSizesSnippet = ` - - -`; - -export const iconButtonDisabledSnippet = ``; - -export const iconButtonResponsiveSnippet = ``; diff --git a/canon-docs/src/snippets/stories-snippets.tsx b/canon-docs/src/snippets/stories-snippets.tsx index 95738a6330..e5a7464b43 100644 --- a/canon-docs/src/snippets/stories-snippets.tsx +++ b/canon-docs/src/snippets/stories-snippets.tsx @@ -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 ? : null; }; +export const ButtonIconSnippet = ({ + story, +}: { + story: keyof typeof ButtonIconStories; +}) => { + const stories = composeStories(ButtonIconStories); + const StoryComponent = stories[story as keyof typeof stories]; + + return StoryComponent ? : null; +}; + +export const ButtonLinkSnippet = ({ + story, +}: { + story: keyof typeof ButtonLinkStories; +}) => { + const stories = composeStories(ButtonLinkStories); + const StoryComponent = stories[story as keyof typeof stories]; + + return StoryComponent ? : null; +}; + export const CheckboxSnippet = ({ story, }: { @@ -96,17 +119,6 @@ export const HeadingSnippet = ({ return StoryComponent ? : null; }; -export const IconButtonSnippet = ({ - story, -}: { - story: keyof typeof IconButtonStories; -}) => { - const stories = composeStories(IconButtonStories); - const StoryComponent = stories[story as keyof typeof stories]; - - return StoryComponent ? : null; -}; - export const IconSnippet = ({ story }: { story: keyof typeof IconStories }) => { const stories = composeStories(IconStories); const StoryComponent = stories[story as keyof typeof stories]; diff --git a/canon-docs/src/utils/data.ts b/canon-docs/src/utils/data.ts index 20ff9146a7..a741ad5160 100644 --- a/canon-docs/src/utils/data.ts +++ b/canon-docs/src/utils/data.ts @@ -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', diff --git a/packages/canon/css/button.css b/packages/canon/css/button.css index 7aaad3ec48..7f15f4a541 100644 --- a/packages/canon/css/button.css +++ b/packages/canon/css/button.css @@ -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; } diff --git a/packages/canon/css/buttonicon.css b/packages/canon/css/buttonicon.css new file mode 100644 index 0000000000..1a791b481f --- /dev/null +++ b/packages/canon/css/buttonicon.css @@ -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; +} diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index d420031ada..327f168696 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -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); diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index b2d111b185..9f32da5d66 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -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); diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 4db3a81bbc..bd0b9cbee0 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -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 & RefAttributes + ButtonProps & RefAttributes >; // @public (undocumented) -export type ButtonOwnProps = GetPropDefTypes; - -// @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 +>; // @public -export interface ButtonProps extends useRender.ComponentProps<'button'> { +export interface ButtonIconProps extends ButtonProps_2 { + // (undocumented) + icon?: ReactElement; + // (undocumented) + size?: 'small' | 'medium' | Partial>; + // (undocumented) + variant?: + | 'primary' + | 'secondary' + | Partial>; +} + +// @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>; + // (undocumented) + variant?: + | 'primary' + | 'secondary' + | Partial>; } // @public (undocumented) @@ -691,40 +698,6 @@ export type HeightProps = GetPropDefTypes; // @public (undocumented) export const Icon: (props: IconProps) => JSX_2.Element | null; -// @public (undocumented) -export const IconButton: ForwardRefExoticComponent< - IconButtonProps & RefAttributes ->; - -// @public (undocumented) -export type IconButtonOwnProps = GetPropDefTypes; - -// @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, 'children'> { - icon: ReactElement; - size?: IconButtonOwnProps['size']; - variant?: IconButtonOwnProps['variant']; -} - // @public (undocumented) export const IconContext: Context; diff --git a/packages/canon/src/components/Button/Button.props.ts b/packages/canon/src/components/Button/Button.props.ts deleted file mode 100644 index e363360afa..0000000000 --- a/packages/canon/src/components/Button/Button.props.ts +++ /dev/null @@ -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; diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx index 2d79da7e29..e020ebb64d 100644 --- a/packages/canon/src/components/Button/Button.stories.tsx +++ b/packages/canon/src/components/Button/Button.stories.tsx @@ -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: () => ( - + ), }; -export const AsLink: Story = { - args: { - children: 'I am a link', - }, - render: args => ( - @@ -194,31 +179,27 @@ export const Playground: Story = { iconStart={} iconEnd={} style={{ width: '200px' }} - variant={variant as ButtonProps['variant']} - size={size as ButtonProps['size']} + variant={variant} + size={size} > Button - diff --git a/packages/canon/src/components/Button/Button.tsx b/packages/canon/src/components/Button/Button.tsx index 9775f3f629..9d256a5815 100644 --- a/packages/canon/src/components/Button/Button.tsx +++ b/packages/canon/src/components/Button/Button.tsx @@ -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( - (props: ButtonProps, ref) => { +export const Button = forwardRef( + (props: ButtonProps, ref: Ref) => { const { size = 'small', variant = 'primary', iconStart, iconEnd, children, - render = + {icon} + ); }, ); -export default IconButton; +ButtonIcon.displayName = 'ButtonIcon'; diff --git a/packages/canon/src/components/Button/index.tsx b/packages/canon/src/components/ButtonIcon/index.tsx similarity index 76% rename from packages/canon/src/components/Button/index.tsx rename to packages/canon/src/components/ButtonIcon/index.tsx index 9051f1145e..686fd844e8 100644 --- a/packages/canon/src/components/Button/index.tsx +++ b/packages/canon/src/components/ButtonIcon/index.tsx @@ -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'; diff --git a/packages/canon/src/components/ButtonIcon/styles.css b/packages/canon/src/components/ButtonIcon/styles.css new file mode 100644 index 0000000000..27c96e696e --- /dev/null +++ b/packages/canon/src/components/ButtonIcon/styles.css @@ -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; +} diff --git a/packages/canon/src/components/IconButton/types.ts b/packages/canon/src/components/ButtonIcon/types.ts similarity index 56% rename from packages/canon/src/components/IconButton/types.ts rename to packages/canon/src/components/ButtonIcon/types.ts index ece1950d1b..7f8b845de4 100644 --- a/packages/canon/src/components/IconButton/types.ts +++ b/packages/canon/src/components/ButtonIcon/types.ts @@ -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, '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>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + icon?: ReactElement; } diff --git a/packages/canon/src/components/ButtonLink/ButtonLink.stories.tsx b/packages/canon/src/components/ButtonLink/ButtonLink.stories.tsx new file mode 100644 index 0000000000..abf6e9d278 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/ButtonLink.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'Button', + }, +}; + +export const Variants: Story = { + render: () => ( + + } + variant="primary" + href="https://canon.backstage.io" + target="_blank" + > + Button + + } + variant="secondary" + href="https://canon.backstage.io" + target="_blank" + > + Button + + + ), +}; + +export const Sizes: Story = { + args: { + children: 'Button', + }, + render: () => ( + + }> + Small + + }> + Medium + + + ), +}; + +export const WithIcons: Story = { + args: { + children: 'Button', + }, + render: args => ( + + } /> + } /> + } + iconEnd={} + /> + + ), +}; + +export const FullWidth: Story = { + args: { + children: 'Button', + }, + render: args => ( + + } /> + } /> + } + iconEnd={} + /> + + ), +}; + +export const Disabled: Story = { + render: () => ( + + + Primary + + + Secondary + + + ), +}; + +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: () => ( + + {variants.map(variant => ( + + {variant} + {sizes.map(size => ( + + + Button + + } + variant={variant} + size={size} + > + Button + + } + variant={variant} + size={size} + > + Button + + } + iconEnd={} + style={{ width: '200px' }} + variant={variant} + size={size} + > + Button + + + Button + + } + variant={variant} + size={size} + isDisabled + > + Button + + } + variant={variant} + size={size} + isDisabled + > + Button + + + ))} + + ))} + + ), +}; diff --git a/packages/canon/src/components/ButtonLink/ButtonLink.tsx b/packages/canon/src/components/ButtonLink/ButtonLink.tsx new file mode 100644 index 0000000000..cb68401160 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/ButtonLink.tsx @@ -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) => { + const { + size = 'small', + variant = 'primary', + iconStart, + iconEnd, + children, + className, + ...rest + } = props; + + const responsiveSize = useResponsiveValue(size); + const responsiveVariant = useResponsiveValue(variant); + + return ( + + {iconStart} + {children} + {iconEnd} + + ); + }, +); + +ButtonLink.displayName = 'ButtonLink'; diff --git a/packages/canon/src/components/ButtonLink/index.ts b/packages/canon/src/components/ButtonLink/index.ts new file mode 100644 index 0000000000..bbfc696c67 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/index.ts @@ -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'; diff --git a/packages/canon/src/components/ButtonLink/types.ts b/packages/canon/src/components/ButtonLink/types.ts new file mode 100644 index 0000000000..f6524fbe62 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/types.ts @@ -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>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + children?: ReactNode; +} diff --git a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx index 2efa73bf14..ee9dda7ab5 100644 --- a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx +++ b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx @@ -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(
{`${fromCount} - ${toCount} of ${rowCount}`} - table?.previousPage()} - disabled={!table?.getCanPreviousPage()} + isDisabled={!table?.getCanPreviousPage()} icon={} /> - table?.nextPage()} - disabled={!table?.getCanNextPage()} + isDisabled={!table?.getCanNextPage()} icon={} />
diff --git a/packages/canon/src/components/IconButton/IconButton.props.ts b/packages/canon/src/components/IconButton/IconButton.props.ts deleted file mode 100644 index e584997ef5..0000000000 --- a/packages/canon/src/components/IconButton/IconButton.props.ts +++ /dev/null @@ -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; diff --git a/packages/canon/src/components/IconButton/styles.css b/packages/canon/src/components/IconButton/styles.css deleted file mode 100644 index 066b0fbad5..0000000000 --- a/packages/canon/src/components/IconButton/styles.css +++ /dev/null @@ -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; -} diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index c85c1bbdd9..aa453f7314 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -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'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index 9b5a454254..69c302bdc5 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -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'; diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index fc6918acea..ca52ec530c 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -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 diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 7e68c4bea9..3ba6047ada 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -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; }; } diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index aac1b0779c..627561b56e 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -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", diff --git a/plugins/catalog-backend/report.api.md b/plugins/catalog-backend/report.api.md index 9bb94b2dd5..6e5ca956bc 100644 --- a/plugins/catalog-backend/report.api.md +++ b/plugins/catalog-backend/report.api.md @@ -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) diff --git a/plugins/catalog-backend/src/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/processors/UrlReaderProcessor.test.ts index 2d7268eb33..0b17ecb19f 100644 --- a/plugins/catalog-backend/src/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/processors/UrlReaderProcessor.test.ts @@ -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 = { - 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, diff --git a/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts index bc383512be..4440a81bae 100644 --- a/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts @@ -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 }; } } diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index c9d75298c8..e50aa84d80 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -356,7 +356,7 @@ export class CatalogBuilder { return [ new FileReaderProcessor(), - new UrlReaderProcessor({ reader, logger, config }), + new UrlReaderProcessor({ reader, logger }), new AnnotateLocationEntityProcessor({ integrations }), ]; } diff --git a/plugins/mcp-actions-backend/README.md b/plugins/mcp-actions-backend/README.md index b14f252bce..c44bd7f0a6 100644 --- a/plugins/mcp-actions-backend/README.md +++ b/plugins/mcp-actions-backend/README.md @@ -89,6 +89,7 @@ backend: - type: static options: token: ${MCP_TOKEN} + subject: mcp-clients accessRestrictions: - plugin: mcp-actions - plugin: catalog diff --git a/plugins/scaffolder-backend-module-cookiecutter/report.api.md b/plugins/scaffolder-backend-module-cookiecutter/report.api.md index 2dfba3f42f..a60a8c0396 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/report.api.md +++ b/plugins/scaffolder-backend-module-cookiecutter/report.api.md @@ -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; + values: objectOutputType<{}, ZodTypeAny, 'passthrough'>; targetPath?: string | undefined; copyWithoutRender?: string[] | undefined; extensions?: string[] | undefined; diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index 3a4ee8c3d7..9a9df95dea 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -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(), { diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.test.tsx new file mode 100644 index 0000000000..80b19f0308 --- /dev/null +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.test.tsx @@ -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
Test
; + }; + + it('should not navigate when defaultPath is undefined', () => { + render(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should navigate when defaultPath is a non-empty string', () => { + render(); + 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(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index 0c4d6dee3e..35e847a091 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -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 diff --git a/yarn.lock b/yarn.lock index c3e37b0ac0..da694cf51f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"