diff --git a/.changeset/fruity-peaches-march.md b/.changeset/fruity-peaches-march.md deleted file mode 100644 index 34303d17e8..0000000000 --- a/.changeset/fruity-peaches-march.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': minor ---- - -The `IconLinkVertical` component now includes an optional hide property. When set to `true`, this property completely hides the icon element. diff --git a/.changeset/large-baboons-prove.md b/.changeset/large-baboons-prove.md index d9b964ef43..18949dd976 100644 --- a/.changeset/large-baboons-prove.md +++ b/.changeset/large-baboons-prove.md @@ -3,3 +3,7 @@ --- Add support to customize the about card icon links via `EntityIconLinkBlueprint` and provide a default catalog view catalog source, launch scaffolder template and read techdocs docs icon links extensions. + +**BREAKING** + +The `Scaffolder` launch template and `TechDocs` read documentation icons have been extracted from the default `Catalog` about card links and are now provided respectively by the `Scaffolder` and `TechDocs` plugins in the new frontend system. If you are trying out the new frontend system and are using a translation for these link titles other than the default, you should now translate them using the scaffolder translation reference or the TechDocs translation reference (the translation keys are still the same, `aboutCard.viewTechdocs` and `aboutCard.launchTemplate`). diff --git a/.changeset/large-crabs-joke.md b/.changeset/large-crabs-joke.md new file mode 100644 index 0000000000..c8e021d09c --- /dev/null +++ b/.changeset/large-crabs-joke.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': minor +--- + +Export a new hook to share how to get the "Launch Template" icon link properties. The function is currently used by the alpha `Scaffolder` plugin and the legacy `Catalog` plugin. +This hook should only be used internally and temporarily until the legacy frontend system is deprecated. diff --git a/.changeset/loose-ants-learn.md b/.changeset/loose-ants-learn.md new file mode 100644 index 0000000000..728bdc8942 --- /dev/null +++ b/.changeset/loose-ants-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +The `TechDocs` plugin is now responsible for providing an entity icon link extension to read documentation from the catalog entity page. diff --git a/.changeset/open-eyes-learn.md b/.changeset/open-eyes-learn.md index 60cc016bc7..3a0299aafb 100644 --- a/.changeset/open-eyes-learn.md +++ b/.changeset/open-eyes-learn.md @@ -4,18 +4,16 @@ Introduces a new `EntityIconLinkBlueprint` that customizes the `About` card icon links on the `Catalog` entity page. -The blueprint currently accepts the following `params`: +The blueprint currently accepts a `useProps` hook as `param` and this function returns the following props that will be passed to the icon link component: -| Name | Description | Type | Default Value | -| ---------- | --------------------------------------------------- | -------------------------- | ------------- | -| `icon` | The icon to display. | `JSX.Element` | N/A | -| `label` | The label for the element. | `string` | N/A | -| `title` | The title for the element. | `string` | N/A | -| `color` | The color of the element. | `'primary' \| 'secondary'` | `primary` | -| `disabled` | Whether the element is disabled. | `boolean` | `false` | -| `href` | The URL to navigate to when the element is clicked. | `string` | N/A | -| `onClick` | A function to call when the element is clicked. | `() => void` | N/A | -| `hidden` | Whether the element is hidden. | `boolean` | `false` | +| Name | Description | Type | Default Value | +| ---------- | --------------------------------------------------- | ------------- | ------------- | +| `icon` | The icon to display. | `JSX.Element` | N/A | +| `label` | The label for the element. | `string` | N/A | +| `title` | The title for the element. | `string` | N/A | +| `disabled` | Whether the element is disabled. | `boolean` | `false` | +| `href` | The URL to navigate to when the element is clicked. | `string` | N/A | +| `onClick` | A function to call when the element is clicked. | `() => void` | N/A | Here is an usage example: @@ -23,32 +21,10 @@ Here is an usage example: import { EntityIconLinkBlueprint } from '@backstage/plugin-catalog-react/alpha'; //... -// Defining the icon link properties using an object EntityIconLinkBlueprint.make({ name: 'my-icon-link', params: { - props: { - label: 'My Icon Link Label', - icon: , - href: '/my-plugin', - }, - }, -}); -``` - -or - -```tsx -import { EntityIconLinkBlueprint } from '@backstage/plugin-catalog-react/alpha'; -//... - -// Defining the icon link properties using a function -EntityIconLinkBlueprint.make({ - name: 'my-icon-link', - params: { - props: function useMyIconLinkProps() { - // Use a function when you would like use a hook for defining the - // icon link props on runtime + useProps() { const { t } = useTranslationRef(myIconLinkTranslationRef); return { label: t('myIconLink.label'), @@ -60,7 +36,7 @@ EntityIconLinkBlueprint.make({ }); ``` -Additionally, the `app-config.yaml` file allows you to override some of the default icon link parameters, including `label`, `title`, `color`, `href`, `disabled`, and `hidden` values. Here's how to set them: +Additionally, the `app-config.yaml` file allows you to override some of the default icon link parameters, including `label` and `title` values. Here's how to set them: ```yaml app: @@ -69,3 +45,5 @@ app: config: label: 'My Custom Icon Link label' ``` + +Finally, you can disable all links if you want to hide the About card header completely (useful, for example, when links are displayed on separate cards). The header is hidden when no icon links extensions are enabled. diff --git a/.changeset/petite-pots-lead.md b/.changeset/petite-pots-lead.md new file mode 100644 index 0000000000..7ce331f5b5 --- /dev/null +++ b/.changeset/petite-pots-lead.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-react': minor +--- + +Export a new hook to share how to get the "View TechDocs" icon link properties. The function is currently used by the alpha `Techdocs` plugin and the legacy `Catalog` plugin. +This hook should only be used internally and temporarily until the legacy frontend system is deprecated. diff --git a/.changeset/rare-rules-move.md b/.changeset/rare-rules-move.md new file mode 100644 index 0000000000..9786bc1104 --- /dev/null +++ b/.changeset/rare-rules-move.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +The `Scaffolder` plugin is now responsible for providing an entity icon link extension to launch templates from the catalog entity page. diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index 56d2662041..90fa9ced22 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -582,8 +582,7 @@ export function IconLinkVertical({ label, onClick, title, - hidden, -}: IconLinkVerticalProps): JSX_2.Element | null; +}: IconLinkVerticalProps): JSX_2.Element; // @public (undocumented) export type IconLinkVerticalClassKey = @@ -604,7 +603,6 @@ export type IconLinkVerticalProps = { label: string; onClick?: MouseEventHandler; title?: string; - hidden?: boolean; }; // @public (undocumented) diff --git a/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.tsx b/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.tsx index 817b7ad5c4..a15e600e8c 100644 --- a/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.tsx +++ b/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.tsx @@ -29,7 +29,6 @@ export type IconLinkVerticalProps = { label: string; onClick?: MouseEventHandler; title?: string; - hidden?: boolean; }; /** @public */ @@ -76,14 +75,9 @@ export function IconLinkVertical({ label, onClick, title, - hidden, }: IconLinkVerticalProps) { const classes = useIconStyles(); - if (hidden) { - return null; - } - if (disabled) { return ( diff --git a/plugins/catalog-import/report-alpha.api.md b/plugins/catalog-import/report-alpha.api.md index d4dab4549a..90716bc442 100644 --- a/plugins/catalog-import/report-alpha.api.md +++ b/plugins/catalog-import/report-alpha.api.md @@ -63,15 +63,15 @@ export const catalogImportTranslationRef: TranslationRef< readonly 'stepInitAnalyzeUrl.error.url': 'Must start with http:// or https://.'; readonly 'stepInitAnalyzeUrl.error.repository': "Couldn't generate entities for your repository"; readonly 'stepInitAnalyzeUrl.error.locations': 'There are no entities at this location'; - readonly 'stepInitAnalyzeUrl.urlHelperText': 'Enter the full path to your entity file to start tracking your component'; readonly 'stepInitAnalyzeUrl.nextButtonText': 'Analyze'; + readonly 'stepInitAnalyzeUrl.urlHelperText': 'Enter the full path to your entity file to start tracking your component'; readonly 'stepPrepareCreatePullRequest.nextButtonText': 'Create PR'; readonly 'stepPrepareCreatePullRequest.previewPr.title': 'Preview Pull Request'; readonly 'stepPrepareCreatePullRequest.previewPr.subheader': 'Create a new Pull Request'; readonly 'stepPrepareCreatePullRequest.previewCatalogInfo.title': 'Preview Entities'; + readonly 'stepPrepareSelectLocations.nextButtonText': 'Review'; readonly 'stepPrepareSelectLocations.locations.description': 'Select one or more locations that are present in your git repository:'; readonly 'stepPrepareSelectLocations.locations.selectAll': 'Select All'; - readonly 'stepPrepareSelectLocations.nextButtonText': 'Review'; readonly 'stepPrepareSelectLocations.existingLocations.description': 'These locations already exist in the catalog:'; readonly 'stepReviewLocation.refresh': 'Refresh'; readonly 'stepReviewLocation.import': 'Import'; diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index cdcfe99c77..e4be5ae299 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -396,36 +396,44 @@ export const EntityIconLinkBlueprint: ExtensionBlueprint<{ kind: 'entity-icon-link'; name: undefined; params: { - props: IconLinkVerticalProps | (() => IconLinkVerticalProps); + useProps: () => Omit; + filter?: EntityPredicate | ((entity: Entity) => boolean); }; - output: ConfigurableExtensionDataRef< - () => IconLinkVerticalProps, - 'entity-icon-link-props', - {} - >; + output: + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + () => IconLinkVerticalProps, + 'entity-icon-link-props', + {} + >; inputs: {}; config: { label: string | undefined; title: string | undefined; - color: 'primary' | 'secondary' | undefined; - href: string | undefined; - hidden: boolean | undefined; - disabled: boolean | undefined; + filter: EntityPredicate | undefined; }; configInput: { - color?: 'primary' | 'secondary' | undefined; - hidden?: boolean | undefined; + filter?: EntityPredicate | undefined; label?: string | undefined; title?: string | undefined; - disabled?: boolean | undefined; - href?: string | undefined; }; dataRefs: { - props: ConfigurableExtensionDataRef< + useProps: ConfigurableExtensionDataRef< () => IconLinkVerticalProps, 'entity-icon-link-props', {} >; + filterFunction: ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + {} + >; }; }>; diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx index 01e49d5d16..fce281b70f 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx @@ -20,6 +20,15 @@ import { createExtensionDataRef, } from '@backstage/frontend-plugin-api'; +import { + EntityPredicate, + entityPredicateToFilterFunction, +} from '../predicates'; +import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema'; + +import { entityFilterFunctionDataRef } from './extensionData'; +import { Entity } from '@backstage/catalog-model'; + const entityIconLinkPropsDataRef = createExtensionDataRef< () => IconLinkVerticalProps >().with({ @@ -30,28 +39,27 @@ const entityIconLinkPropsDataRef = createExtensionDataRef< export const EntityIconLinkBlueprint = createExtensionBlueprint({ kind: 'entity-icon-link', attachTo: { id: 'entity-card:catalog/about', input: 'iconLinks' }, - output: [entityIconLinkPropsDataRef], + output: [entityIconLinkPropsDataRef, entityFilterFunctionDataRef.optional()], dataRefs: { - props: entityIconLinkPropsDataRef, + useProps: entityIconLinkPropsDataRef, + filterFunction: entityFilterFunctionDataRef, }, config: { schema: { label: z => z.string().optional(), title: z => z.string().optional(), - color: z => z.enum(['primary', 'secondary']).optional(), - href: z => z.string().optional(), - hidden: z => z.boolean().optional(), - disabled: z => z.boolean().optional(), + filter: z => createEntityPredicateSchema(z).optional(), }, }, *factory( params: { - props: IconLinkVerticalProps | (() => IconLinkVerticalProps); + useProps: () => Omit; + filter?: EntityPredicate | ((entity: Entity) => boolean); }, { config }, ) { yield entityIconLinkPropsDataRef(() => ({ - ...(typeof params.props === 'function' ? params.props() : params.props), + ...params.useProps(), ...Object.entries(config).reduce((rest, [key, value]) => { // Only include properties that are defined in the config // to avoid overriding defaults with undefined values @@ -59,13 +67,22 @@ export const EntityIconLinkBlueprint = createExtensionBlueprint({ return { ...rest, [key]: value, - // Removing the "onClick" handler if href is provided prevents the handler - // from redirecting to a different page - ...(key === 'href' ? { onClick: undefined } : {}), }; } return rest; }, {}), })); + + if (config.filter) { + yield entityFilterFunctionDataRef( + entityPredicateToFilterFunction(config.filter), + ); + } else if (typeof params.filter === 'function') { + yield entityFilterFunctionDataRef(params.filter); + } else if (params.filter) { + yield entityFilterFunctionDataRef( + entityPredicateToFilterFunction(params.filter), + ); + } }, }); diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 1e78d40306..29c4eb134b 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -70,6 +70,7 @@ "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", + "@backstage/plugin-scaffolder-react": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-search-react": "workspace:^", "@backstage/plugin-techdocs-common": "workspace:^", diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index ca501cb107..d7a7be0778 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -370,11 +370,18 @@ const _default: FrontendPlugin< >; inputs: { iconLinks: ExtensionInput< - ConfigurableExtensionDataRef< - () => IconLinkVerticalProps, - 'entity-icon-link-props', - {} - >, + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + () => IconLinkVerticalProps, + 'entity-icon-link-props', + {} + >, { singleton: false; optional: false; @@ -936,91 +943,36 @@ const _default: FrontendPlugin< inputs: {}; params: EntityContextMenuItemParams; }>; - 'entity-icon-link:catalog/catalog-view-source': ExtensionDefinition<{ + 'entity-icon-link:catalog/view-source': ExtensionDefinition<{ kind: 'entity-icon-link'; - name: 'catalog-view-source'; + name: 'view-source'; config: { label: string | undefined; title: string | undefined; - color: 'primary' | 'secondary' | undefined; - href: string | undefined; - hidden: boolean | undefined; - disabled: boolean | undefined; + filter: EntityPredicate | undefined; }; configInput: { - color?: 'primary' | 'secondary' | undefined; - hidden?: boolean | undefined; + filter?: EntityPredicate | undefined; label?: string | undefined; title?: string | undefined; - disabled?: boolean | undefined; - href?: string | undefined; }; - output: ConfigurableExtensionDataRef< - () => IconLinkVerticalProps, - 'entity-icon-link-props', - {} - >; + output: + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + () => IconLinkVerticalProps, + 'entity-icon-link-props', + {} + >; inputs: {}; params: { - props: IconLinkVerticalProps | (() => IconLinkVerticalProps); - }; - }>; - 'entity-icon-link:catalog/scaffolder-launch-template': ExtensionDefinition<{ - kind: 'entity-icon-link'; - name: 'scaffolder-launch-template'; - config: { - label: string | undefined; - title: string | undefined; - color: 'primary' | 'secondary' | undefined; - href: string | undefined; - hidden: boolean | undefined; - disabled: boolean | undefined; - }; - configInput: { - color?: 'primary' | 'secondary' | undefined; - hidden?: boolean | undefined; - label?: string | undefined; - title?: string | undefined; - disabled?: boolean | undefined; - href?: string | undefined; - }; - output: ConfigurableExtensionDataRef< - () => IconLinkVerticalProps, - 'entity-icon-link-props', - {} - >; - inputs: {}; - params: { - props: IconLinkVerticalProps | (() => IconLinkVerticalProps); - }; - }>; - 'entity-icon-link:catalog/techdocs-view-documentation': ExtensionDefinition<{ - kind: 'entity-icon-link'; - name: 'techdocs-view-documentation'; - config: { - label: string | undefined; - title: string | undefined; - color: 'primary' | 'secondary' | undefined; - href: string | undefined; - hidden: boolean | undefined; - disabled: boolean | undefined; - }; - configInput: { - color?: 'primary' | 'secondary' | undefined; - hidden?: boolean | undefined; - label?: string | undefined; - title?: string | undefined; - disabled?: boolean | undefined; - href?: string | undefined; - }; - output: ConfigurableExtensionDataRef< - () => IconLinkVerticalProps, - 'entity-icon-link-props', - {} - >; - inputs: {}; - params: { - props: IconLinkVerticalProps | (() => IconLinkVerticalProps); + useProps: () => Omit; + filter?: EntityPredicate | ((entity: Entity) => boolean); }; }>; 'nav-item:catalog': ExtensionDefinition<{ diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index 556b540b92..1b9b4dfa8c 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -40,13 +40,10 @@ import { TableProps } from '@backstage/core-components'; import { TabProps } from '@material-ui/core/Tab'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; +// Warning: (ae-forgotten-export) The symbol "InternalAboutCardProps" needs to be exported by the entry point index.d.ts +// // @public -export interface AboutCardProps { - // (undocumented) - subheader?: JSX.Element; - // (undocumented) - variant?: InfoCardVariants; -} +export type AboutCardProps = Pick; // @public (undocumented) export function AboutContent(props: AboutContentProps): JSX_2.Element; diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index 775959bef0..6533bde3a1 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -20,28 +20,45 @@ import { } from '@backstage/plugin-catalog-react/alpha'; import { compatWrapper } from '@backstage/core-compat-api'; import { createExtensionInput } from '@backstage/frontend-plugin-api'; -import { HeaderIconLinkRow } from '@backstage/core-components'; +import { + HeaderIconLinkRow, + IconLinkVerticalProps, +} from '@backstage/core-components'; +import { useEntity } from '@backstage/plugin-catalog-react'; export const catalogAboutEntityCard = EntityCardBlueprint.makeWithOverrides({ name: 'about', inputs: { - iconLinks: createExtensionInput([EntityIconLinkBlueprint.dataRefs.props]), + iconLinks: createExtensionInput([ + EntityIconLinkBlueprint.dataRefs.useProps, + EntityIconLinkBlueprint.dataRefs.filterFunction.optional(), + ]), }, factory(originalFactory, { inputs }) { function Subheader() { - // The props input functions may be calling other hooks, so we need to + const { entity } = useEntity(); + // The "useProps" functions may be calling other hooks, so we need to // call them in a component function to avoid breaking the rules of hooks. - const links = inputs.iconLinks.map(iconLink => - iconLink.get(EntityIconLinkBlueprint.dataRefs.props)(), - ); - return ; + const links = inputs.iconLinks.reduce((rest, iconLink) => { + const props = iconLink.get(EntityIconLinkBlueprint.dataRefs.useProps)(); + const filter = + iconLink.get(EntityIconLinkBlueprint.dataRefs.filterFunction) ?? + (() => true); + if (filter(entity)) { + return [...rest, props]; + } + return rest; + }, new Array()); + return links.length ? : null; } return originalFactory({ type: 'info', async loader() { - const { AboutCard } = await import('../components/AboutCard'); + const { InternalAboutCard } = await import( + '../components/AboutCard/AboutCard' + ); return compatWrapper( - } />, + } />, ); }, }); diff --git a/plugins/catalog/src/alpha/entityIconLinks.tsx b/plugins/catalog/src/alpha/entityIconLinks.tsx index 21595ad258..b13b13477c 100644 --- a/plugins/catalog/src/alpha/entityIconLinks.tsx +++ b/plugins/catalog/src/alpha/entityIconLinks.tsx @@ -15,35 +15,13 @@ */ import { EntityIconLinkBlueprint } from '@backstage/plugin-catalog-react/alpha'; -import { - useCatalogViewSourceEntityIconLinkProps, - useScaffolderLaunchTemplateEntityIconLinkProps, - useTechdocsViewDocumentionIconLinkProps, -} from '../components/AboutCard/AboutCard'; +import { useCatalogSourceIconLinkProps } from '../components/AboutCard/AboutCard'; const catalogViewSourceEntityIconLink = EntityIconLinkBlueprint.make({ - name: 'catalog-view-source', + name: 'view-source', params: { - props: useCatalogViewSourceEntityIconLinkProps, + useProps: useCatalogSourceIconLinkProps, }, }); -const techdocsViewDocumentationAboutEntityLink = EntityIconLinkBlueprint.make({ - name: 'techdocs-view-documentation', - params: { - props: useTechdocsViewDocumentionIconLinkProps, - }, -}); - -const scaffolderLaunchTemplateEntityIconLink = EntityIconLinkBlueprint.make({ - name: 'scaffolder-launch-template', - params: { - props: useScaffolderLaunchTemplateEntityIconLinkProps, - }, -}); - -export default [ - catalogViewSourceEntityIconLink, - techdocsViewDocumentationAboutEntityLink, - scaffolderLaunchTemplateEntityIconLink, -]; +export default [catalogViewSourceEntityIconLink]; diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 1c093a049c..e5d6bbeef2 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -16,7 +16,6 @@ import { ANNOTATION_EDIT_URL, ANNOTATION_LOCATION, - DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; import Card from '@material-ui/core/Card'; @@ -40,7 +39,6 @@ import { alertApiRef, errorApiRef, useApi, - useApp, useRouteRef, } from '@backstage/core-plugin-api'; import { @@ -52,24 +50,17 @@ import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { AboutContent } from './AboutContent'; import CachedIcon from '@material-ui/icons/Cached'; -import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; -import DocsIcon from '@material-ui/icons/Description'; import EditIcon from '@material-ui/icons/Edit'; -import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha'; import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha'; import { useSourceTemplateCompoundEntityRef } from './hooks'; -import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha'; -import { usePermission } from '@backstage/plugin-permission-react'; import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { buildTechDocsURL } from '@backstage/plugin-techdocs-react'; -import { - TECHDOCS_ANNOTATION, - TECHDOCS_EXTERNAL_ANNOTATION, -} from '@backstage/plugin-techdocs-common'; +import { useTechdocsReaderIconLinkProps } from '@backstage/plugin-techdocs-react/alpha'; +import { useScaffolderTemplateIconLinkProps } from '@backstage/plugin-scaffolder-react/alpha'; +import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -export function useCatalogViewSourceEntityIconLinkProps() { +export function useCatalogSourceIconLinkProps() { const { entity } = useEntity(); const scmIntegrationsApi = useApi(scmIntegrationsApiRef); const { t } = useTranslationRef(catalogTranslationRef); @@ -85,45 +76,24 @@ export function useCatalogViewSourceEntityIconLinkProps() { }; } -export function useTechdocsViewDocumentionIconLinkProps() { +function DefaultAboutCardSubheader() { const { entity } = useEntity(); - const viewTechdocLink = useRouteRef(viewTechDocRouteRef); - const { t } = useTranslationRef(catalogTranslationRef); - - return { - label: t('aboutCard.viewTechdocs'), - disabled: - !( - entity.metadata.annotations?.[TECHDOCS_ANNOTATION] || - entity.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION] - ) || !viewTechdocLink, - icon: , - href: buildTechDocsURL(entity, viewTechdocLink), - }; -} - -export function useScaffolderLaunchTemplateEntityIconLinkProps() { - const app = useApp(); - const { entity } = useEntity(); - const templateRoute = useRouteRef(createFromTemplateRouteRef); - const { t } = useTranslationRef(catalogTranslationRef); - const Icon = app.getSystemIcon('scaffolder') ?? CreateComponentIcon; - const { allowed: canCreateTemplateTask } = usePermission({ - permission: taskCreatePermission, + const catalogSourceIconLink = useCatalogSourceIconLinkProps(); + const techdocsreaderIconLink = useTechdocsReaderIconLinkProps({ + translationRef: catalogTranslationRef, + externalRouteRef: viewTechDocRouteRef, + }); + const scaffolderTemplateIconLink = useScaffolderTemplateIconLinkProps({ + translationRef: catalogTranslationRef, + externalRouteRef: createFromTemplateRouteRef, }); - return { - label: t('aboutCard.launchTemplate'), - icon: , - hidden: !isTemplateEntityV1beta3(entity), - disabled: !templateRoute || !canCreateTemplateTask, - href: - templateRoute && - templateRoute({ - templateName: entity.metadata.name, - namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, - }), - }; + const links = [catalogSourceIconLink, techdocsreaderIconLink]; + if (isTemplateEntityV1beta3(entity)) { + links.push(scaffolderTemplateIconLink); + } + + return ; } const useStyles = makeStyles({ @@ -146,24 +116,12 @@ const useStyles = makeStyles({ }, }); -/** - * Props for {@link EntityAboutCard}. - * - * @public - */ -export interface AboutCardProps { +export interface InternalAboutCardProps { variant?: InfoCardVariants; subheader?: JSX.Element; } -/** - * Exported publicly via the EntityAboutCard - * - * NOTE: We generally do not accept pull requests to extend this class with more - * props and customizability. If you need to tweak it, consider making a bespoke - * card in your own repository instead, that is perfect for your own needs. - */ -export function AboutCard(props: AboutCardProps) { +export function InternalAboutCard(props: InternalAboutCardProps) { const { variant, subheader } = props; const classes = useStyles(); const { entity } = useEntity(); @@ -180,12 +138,6 @@ export function AboutCard(props: AboutCardProps) { const entityMetadataEditUrl = entity.metadata.annotations?.[ANNOTATION_EDIT_URL]; - const viewCatalogSourceIconLink = useCatalogViewSourceEntityIconLinkProps(); - const viewTechdocsDocumentationIconLink = - useTechdocsViewDocumentionIconLinkProps(); - const launchScaffolderTemplateIconLink = - useScaffolderLaunchTemplateEntityIconLinkProps(); - let cardClass = ''; if (variant === 'gridItem') { cardClass = classes.gridItemCard; @@ -255,17 +207,7 @@ export function AboutCard(props: AboutCardProps) { )} } - subheader={ - subheader ?? ( - - ) - } + subheader={subheader ?? } /> @@ -274,3 +216,21 @@ export function AboutCard(props: AboutCardProps) { ); } + +/** + * Props for {@link EntityAboutCard}. + * + * @public + */ +export type AboutCardProps = Pick; + +/** + * Exported publicly via the EntityAboutCard + * + * NOTE: We generally do not accept pull requests to extend this class with more + * props and customizability. If you need to tweak it, consider making a bespoke + * card in your own repository instead, that is perfect for your own needs. + */ +export function AboutCard(props: AboutCardProps) { + return ; +} diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index 2014bff9be..3a1e330614 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -14,6 +14,7 @@ import { Dispatch } from 'react'; import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react'; import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import { FieldSchema } from '@backstage/plugin-scaffolder-react'; @@ -344,8 +345,8 @@ export const scaffolderReactTranslationRef: TranslationRef< readonly 'stepper.backButtonText': 'Back'; readonly 'stepper.createButtonText': 'Create'; readonly 'stepper.reviewButtonText': 'Review'; - readonly 'stepper.nextButtonText': 'Next'; readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; + readonly 'stepper.nextButtonText': 'Next'; readonly 'templateCategoryPicker.title': 'Categories'; readonly 'templateCard.noDescription': 'No description'; readonly 'templateCard.chooseButtonText': 'Choose'; @@ -484,6 +485,17 @@ export const useFormDataFromQuery: ( initialState?: Record, ) => [Record, Dispatch>>]; +// @alpha (undocumented) +export function useScaffolderTemplateIconLinkProps(options: { + translationRef: TranslationRef; + externalRouteRef: ExternalRouteRef; +}): { + label: string; + icon: JSX_2.Element; + disabled: boolean; + href: string | undefined; +}; + // @alpha (undocumented) export const useTemplateParameterSchema: (templateRef: string) => { manifest?: TemplateParameterSchema; diff --git a/plugins/scaffolder-react/src/alpha.ts b/plugins/scaffolder-react/src/alpha.ts index e18c758b3d..1b4935c522 100644 --- a/plugins/scaffolder-react/src/alpha.ts +++ b/plugins/scaffolder-react/src/alpha.ts @@ -17,3 +17,5 @@ export * from './next'; export { scaffolderReactTranslationRef } from './translation'; + +export { useScaffolderTemplateIconLinkProps } from './hooks/useScaffolderTemplateIconLinkProps'; diff --git a/plugins/scaffolder-react/src/hooks/useScaffolderTemplateIconLinkProps.tsx b/plugins/scaffolder-react/src/hooks/useScaffolderTemplateIconLinkProps.tsx new file mode 100644 index 0000000000..b59f858ce9 --- /dev/null +++ b/plugins/scaffolder-react/src/hooks/useScaffolderTemplateIconLinkProps.tsx @@ -0,0 +1,59 @@ +/* + * 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 { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { + ExternalRouteRef, + useApp, + useRouteRef, +} from '@backstage/core-plugin-api'; +import { useEntity } from '@backstage/plugin-catalog-react'; + +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; +import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha'; +import { usePermission } from '@backstage/plugin-permission-react'; +import { + TranslationRef, + useTranslationRef, +} from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export function useScaffolderTemplateIconLinkProps(options: { + translationRef: TranslationRef; + externalRouteRef: ExternalRouteRef; +}) { + const { translationRef, externalRouteRef } = options; + const app = useApp(); + const { entity } = useEntity(); + const templateRoute = useRouteRef(externalRouteRef); + const { t } = useTranslationRef(translationRef); + const Icon = app.getSystemIcon('scaffolder') ?? CreateComponentIcon; + const { allowed: canCreateTemplateTask } = usePermission({ + permission: taskCreatePermission, + }); + + return { + label: t('aboutCard.launchTemplate'), + icon: , + disabled: !templateRoute || !canCreateTemplateTask, + href: + templateRoute && + templateRoute({ + templateName: entity.metadata.name, + namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, + }), + }; +} diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index 69fcec11e6..05e94a446a 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -8,6 +8,8 @@ import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiRef } from '@backstage/frontend-plugin-api'; import { ComponentType } from 'react'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; @@ -18,6 +20,7 @@ import type { FormProps as FormProps_2 } from '@rjsf/core'; import { FormProps as FormProps_3 } from '@backstage/plugin-scaffolder-react'; import { FrontendPlugin } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; +import { IconLinkVerticalProps } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react'; import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; import { PathParams } from '@backstage/core-plugin-api'; @@ -45,6 +48,10 @@ const _default: FrontendPlugin< }, { registerComponent: ExternalRouteRef; + createFromTemplate: ExternalRouteRef<{ + namespace: string; + templateName: string; + }>; viewTechDoc: ExternalRouteRef<{ name: string; kind: string; @@ -121,6 +128,38 @@ const _default: FrontendPlugin< factory: AnyApiFactory; }; }>; + 'entity-icon-link:scaffolder/launch-template': ExtensionDefinition<{ + kind: 'entity-icon-link'; + name: 'launch-template'; + config: { + label: string | undefined; + title: string | undefined; + filter: EntityPredicate | undefined; + }; + configInput: { + filter?: EntityPredicate | undefined; + label?: string | undefined; + title?: string | undefined; + }; + output: + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + () => IconLinkVerticalProps, + 'entity-icon-link-props', + {} + >; + inputs: {}; + params: { + useProps: () => Omit; + filter?: EntityPredicate | ((entity: Entity) => boolean); + }; + }>; 'nav-item:scaffolder': ExtensionDefinition<{ kind: 'nav-item'; name: undefined; @@ -325,6 +364,7 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'fields.repoUrlPicker.repository.title': 'Repositories Available'; readonly 'fields.repoUrlPicker.repository.description': 'The name of the repository'; readonly 'fields.repoUrlPicker.repository.inputTitle': 'Repository'; + readonly 'aboutCard.launchTemplate': 'Launch Template'; readonly 'actionsPage.content.emptyState.title': 'No information to display'; readonly 'actionsPage.content.emptyState.description': 'There are no actions installed or there was an issue communicating with backend.'; readonly 'actionsPage.content.searchFieldPlaceholder': 'Search for an action'; diff --git a/plugins/scaffolder/src/alpha/plugin.tsx b/plugins/scaffolder/src/alpha/plugin.tsx index ebad851901..56f7343978 100644 --- a/plugins/scaffolder/src/alpha/plugin.tsx +++ b/plugins/scaffolder/src/alpha/plugin.tsx @@ -26,6 +26,7 @@ import { selectedTemplateRouteRef, templatingExtensionsRouteRef, viewTechDocRouteRef, + createFromTemplateRouteRef, } from '../routes'; import { repoUrlPickerFormField, @@ -33,8 +34,28 @@ import { scaffolderPage, scaffolderApi, } from './extensions'; -import { formFieldsApi } from '@backstage/plugin-scaffolder-react/alpha'; +import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { + formFieldsApi, + useScaffolderTemplateIconLinkProps, +} from '@backstage/plugin-scaffolder-react/alpha'; import { formDecoratorsApi } from './api'; +import { EntityIconLinkBlueprint } from '@backstage/plugin-catalog-react/alpha'; +import { scaffolderTranslationRef } from '../translation'; + +/** @alpha */ +const scaffolderEntityIconLink = EntityIconLinkBlueprint.make({ + name: 'launch-template', + params: { + filter: isTemplateEntityV1beta3, + useProps: () => { + return useScaffolderTemplateIconLinkProps({ + translationRef: scaffolderTranslationRef, + externalRouteRef: createFromTemplateRouteRef, + }); + }, + }, +}); /** @alpha */ export default createFrontendPlugin({ @@ -51,12 +72,14 @@ export default createFrontendPlugin({ }), externalRoutes: convertLegacyRouteRefs({ registerComponent: registerComponentRouteRef, + createFromTemplate: createFromTemplateRouteRef, viewTechDoc: viewTechDocRouteRef, }), extensions: [ scaffolderApi, scaffolderPage, scaffolderNavItem, + scaffolderEntityIconLink, formDecoratorsApi, formFieldsApi, repoUrlPickerFormField, diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index 2e336757b8..0a36089928 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -32,6 +32,13 @@ export const viewTechDocRouteRef = createExternalRouteRef({ defaultTarget: 'techdocs.docRoot', }); +export const createFromTemplateRouteRef = createExternalRouteRef({ + id: 'create-from-template', + optional: true, + params: ['namespace', 'templateName'], + defaultTarget: 'scaffolder.selectedTemplate', +}); + /** * @public */ diff --git a/plugins/scaffolder/src/translation.ts b/plugins/scaffolder/src/translation.ts index 2354410924..fc0f0a6889 100644 --- a/plugins/scaffolder/src/translation.ts +++ b/plugins/scaffolder/src/translation.ts @@ -19,6 +19,9 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; export const scaffolderTranslationRef = createTranslationRef({ id: 'scaffolder', messages: { + aboutCard: { + launchTemplate: 'Launch Template', + }, actionsPage: { title: 'Installed actions', pageTitle: 'Create a New Component', diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index 618af87698..774b3e4dae 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -63,9 +63,11 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-techdocs-common": "workspace:^", "@backstage/version-bridge": "workspace:^", "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", "@material-ui/styles": "^4.11.0", "jss": "~10.10.0", "lodash": "^4.17.21", diff --git a/plugins/techdocs-react/report-alpha.api.md b/plugins/techdocs-react/report-alpha.api.md index d916d1fcb0..f4f4e612ed 100644 --- a/plugins/techdocs-react/report-alpha.api.md +++ b/plugins/techdocs-react/report-alpha.api.md @@ -6,6 +6,9 @@ import { ComponentType } from 'react'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; +import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react/jsx-runtime'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @alpha export const AddonBlueprint: ExtensionBlueprint<{ @@ -59,5 +62,16 @@ export type TechDocsAddonOptions = { component: ComponentType; }; +// @alpha (undocumented) +export function useTechdocsReaderIconLinkProps(options: { + translationRef: TranslationRef; + externalRouteRef: ExternalRouteRef; +}): { + label: string; + disabled: boolean; + icon: JSX_2.Element; + href: string | undefined; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/techdocs-react/src/alpha.ts b/plugins/techdocs-react/src/alpha.ts index 83c0444753..2cf7878047 100644 --- a/plugins/techdocs-react/src/alpha.ts +++ b/plugins/techdocs-react/src/alpha.ts @@ -26,6 +26,8 @@ import { createExtensionDataRef, } from '@backstage/frontend-plugin-api'; +export { useTechdocsReaderIconLinkProps } from './hooks'; + /** @alpha */ export type { TechDocsAddonOptions, TechDocsAddonLocations } from './types'; diff --git a/plugins/techdocs-react/src/hooks.ts b/plugins/techdocs-react/src/hooks.tsx similarity index 76% rename from plugins/techdocs-react/src/hooks.ts rename to plugins/techdocs-react/src/hooks.tsx index 1203622cb2..b8819a345e 100644 --- a/plugins/techdocs-react/src/hooks.ts +++ b/plugins/techdocs-react/src/hooks.tsx @@ -15,7 +15,45 @@ */ import { useEffect, useMemo, useState } from 'react'; import debounce from 'lodash/debounce'; +import DocsIcon from '@material-ui/icons/Description'; + +import { ExternalRouteRef, useRouteRef } from '@backstage/core-plugin-api'; +import { + TranslationRef, + useTranslationRef, +} from '@backstage/core-plugin-api/alpha'; + +import { + TECHDOCS_ANNOTATION, + TECHDOCS_EXTERNAL_ANNOTATION, +} from '@backstage/plugin-techdocs-common'; + +import { useEntity } from '@backstage/plugin-catalog-react'; + import { useTechDocsReaderPage } from './context'; +import { buildTechDocsURL } from './helpers'; + +/** @alpha */ +export function useTechdocsReaderIconLinkProps(options: { + translationRef: TranslationRef; + externalRouteRef: ExternalRouteRef; +}) { + const { translationRef, externalRouteRef } = options; + const { entity } = useEntity(); + const viewTechdocLink = useRouteRef(externalRouteRef); + const { t } = useTranslationRef(translationRef); + + return { + label: t('aboutCard.viewTechdocs'), + disabled: + !( + entity.metadata.annotations?.[TECHDOCS_ANNOTATION] || + entity.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION] + ) || !viewTechdocLink, + icon: , + href: buildTechDocsURL(entity, viewTechdocLink), + }; +} /** * Hook for use within TechDocs addons that provides access to the underlying diff --git a/plugins/techdocs/report-alpha.api.md b/plugins/techdocs/report-alpha.api.md index e34046ad2e..69e98a4c33 100644 --- a/plugins/techdocs/report-alpha.api.md +++ b/plugins/techdocs/report-alpha.api.md @@ -12,14 +12,17 @@ import { Entity } from '@backstage/catalog-model'; import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; import { FrontendPlugin } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; +import { IconLinkVerticalProps } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha'; import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha'; import { SearchResultListItemBlueprintParams } from '@backstage/plugin-search-react/alpha'; import { TechDocsAddonOptions } from '@backstage/plugin-techdocs-react'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) const _default: FrontendPlugin< @@ -32,7 +35,13 @@ const _default: FrontendPlugin< }>; entityContent: RouteRef; }, - {}, + { + viewTechDoc: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + }, { 'api:techdocs': ExtensionDefinition<{ kind: 'api'; @@ -173,6 +182,38 @@ const _default: FrontendPlugin< filter?: string | EntityPredicate | ((entity: Entity) => boolean); }; }>; + 'entity-icon-link:techdocs/read-docs': ExtensionDefinition<{ + kind: 'entity-icon-link'; + name: 'read-docs'; + config: { + label: string | undefined; + title: string | undefined; + filter: EntityPredicate | undefined; + }; + configInput: { + filter?: EntityPredicate | undefined; + label?: string | undefined; + title?: string | undefined; + }; + output: + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + () => IconLinkVerticalProps, + 'entity-icon-link-props', + {} + >; + inputs: {}; + params: { + useProps: () => Omit; + filter?: EntityPredicate | ((entity: Entity) => boolean); + }; + }>; 'nav-item:techdocs': ExtensionDefinition<{ kind: 'nav-item'; name: undefined; @@ -340,5 +381,13 @@ export const techDocsSearchResultListItemExtension: ExtensionDefinition<{ params: SearchResultListItemBlueprintParams; }>; +// @alpha (undocumented) +export const techdocsTranslationRef: TranslationRef< + 'techdocs', + { + readonly 'aboutCard.viewTechdocs': 'View TechDocs'; + } +>; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx index 21b594c82d..37cdb64ead 100644 --- a/plugins/techdocs/src/alpha.tsx +++ b/plugins/techdocs/src/alpha.tsx @@ -35,7 +35,10 @@ import { convertLegacyRouteRef, convertLegacyRouteRefs, } from '@backstage/core-compat-api'; -import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; +import { + EntityContentBlueprint, + EntityIconLinkBlueprint, +} from '@backstage/plugin-catalog-react/alpha'; import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; import { TechDocsClient, TechDocsStorageClient } from './client'; @@ -43,15 +46,36 @@ import { rootCatalogDocsRouteRef, rootDocsRouteRef, rootRouteRef, + viewTechDocRouteRef, } from './routes'; import { TechDocsReaderLayout } from './reader'; -import { attachTechDocsAddonComponentData } from '@backstage/plugin-techdocs-react/alpha'; +import { + attachTechDocsAddonComponentData, + useTechdocsReaderIconLinkProps, +} from '@backstage/plugin-techdocs-react/alpha'; import { TechDocsAddons, techdocsApiRef, techdocsStorageApiRef, } from '@backstage/plugin-techdocs-react'; +import { techdocsTranslationRef } from './translation'; + +export { techdocsTranslationRef } from './translation'; + +/** @alpha */ +const techdocsEntityIconLink = EntityIconLinkBlueprint.make({ + name: 'read-docs', + params: { + useProps: () => { + return useTechdocsReaderIconLinkProps({ + translationRef: techdocsTranslationRef, + externalRouteRef: viewTechDocRouteRef, + }); + }, + }, +}); + /** @alpha */ const techDocsStorageApi = ApiBlueprint.make({ name: 'storage', @@ -243,6 +267,7 @@ export default createFrontendPlugin({ techDocsNavItem, techDocsPage, techDocsReaderPage, + techdocsEntityIconLink, techDocsEntityContent, techDocsEntityContentEmptyState, techDocsSearchResultListItemExtension, @@ -252,4 +277,7 @@ export default createFrontendPlugin({ docRoot: rootDocsRouteRef, entityContent: rootCatalogDocsRouteRef, }), + externalRoutes: convertLegacyRouteRefs({ + viewTechDoc: viewTechDocRouteRef, + }), }); diff --git a/plugins/techdocs/src/routes.ts b/plugins/techdocs/src/routes.ts index e781550899..69f76a1627 100644 --- a/plugins/techdocs/src/routes.ts +++ b/plugins/techdocs/src/routes.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { createRouteRef } from '@backstage/core-plugin-api'; +import { + createRouteRef, + createExternalRouteRef, +} from '@backstage/core-plugin-api'; export const rootRouteRef = createRouteRef({ id: 'techdocs:index-page', @@ -28,3 +31,10 @@ export const rootDocsRouteRef = createRouteRef({ export const rootCatalogDocsRouteRef = createRouteRef({ id: 'techdocs:catalog-reader-view', }); + +export const viewTechDocRouteRef = createExternalRouteRef({ + id: 'view-techdoc', + optional: true, + params: ['namespace', 'kind', 'name'], + defaultTarget: 'techdocs.docRoot', +}); diff --git a/plugins/techdocs/src/translation.ts b/plugins/techdocs/src/translation.ts new file mode 100644 index 0000000000..aa62246788 --- /dev/null +++ b/plugins/techdocs/src/translation.ts @@ -0,0 +1,27 @@ +/* + * 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 { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const techdocsTranslationRef = createTranslationRef({ + id: 'techdocs', + messages: { + aboutCard: { + viewTechdocs: 'View TechDocs', + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index ab61b20c89..a7ca1e5817 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6378,6 +6378,7 @@ __metadata: "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" + "@backstage/plugin-scaffolder-react": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" "@backstage/plugin-techdocs-common": "workspace:^" @@ -8531,11 +8532,13 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-techdocs-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/version-bridge": "workspace:^" "@material-ui/core": "npm:^4.12.2" + "@material-ui/icons": "npm:^4.9.1" "@material-ui/styles": "npm:^4.11.0" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0"