From 406acb6d61cb65760c75c06589cad26c81a48195 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 26 May 2025 09:09:02 +0200 Subject: [PATCH 1/3] feat: create entity icon link extensions Signed-off-by: Camila Belo --- .changeset/fruity-peaches-march.md | 5 + .changeset/large-baboons-prove.md | 5 + .changeset/open-eyes-learn.md | 71 ++++++++++ packages/core-components/report.api.md | 4 +- .../HeaderIconLinkRow/IconLinkVertical.tsx | 6 + plugins/catalog-react/report-alpha.api.md | 39 ++++++ .../blueprints/EntityIconLinkBlueprint.tsx | 71 ++++++++++ .../src/alpha/blueprints/index.ts | 1 + plugins/catalog/report-alpha.api.md | 106 ++++++++++++++- plugins/catalog/report.api.md | 2 + plugins/catalog/src/alpha/entityCards.tsx | 36 +++-- plugins/catalog/src/alpha/entityIconLinks.tsx | 49 +++++++ plugins/catalog/src/alpha/plugin.tsx | 2 + .../src/components/AboutCard/AboutCard.tsx | 125 +++++++++++------- 14 files changed, 460 insertions(+), 62 deletions(-) create mode 100644 .changeset/fruity-peaches-march.md create mode 100644 .changeset/large-baboons-prove.md create mode 100644 .changeset/open-eyes-learn.md create mode 100644 plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx create mode 100644 plugins/catalog/src/alpha/entityIconLinks.tsx diff --git a/.changeset/fruity-peaches-march.md b/.changeset/fruity-peaches-march.md new file mode 100644 index 0000000000..34303d17e8 --- /dev/null +++ b/.changeset/fruity-peaches-march.md @@ -0,0 +1,5 @@ +--- +'@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 new file mode 100644 index 0000000000..d9b964ef43 --- /dev/null +++ b/.changeset/large-baboons-prove.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +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. diff --git a/.changeset/open-eyes-learn.md b/.changeset/open-eyes-learn.md new file mode 100644 index 0000000000..60cc016bc7 --- /dev/null +++ b/.changeset/open-eyes-learn.md @@ -0,0 +1,71 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Introduces a new `EntityIconLinkBlueprint` that customizes the `About` card icon links on the `Catalog` entity page. + +The blueprint currently accepts the following `params`: + +| 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` | + +Here is an usage example: + +```tsx +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 + const { t } = useTranslationRef(myIconLinkTranslationRef); + return { + label: t('myIconLink.label'), + icon: , + href: '/my-plugin', + }; + }, + }, +}); +``` + +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: + +```yaml +app: + extensions: + - entity-icon-link:my-plugin/my-icon-link: + config: + label: 'My Custom Icon Link label' +``` diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index 90fa9ced22..56d2662041 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -582,7 +582,8 @@ export function IconLinkVertical({ label, onClick, title, -}: IconLinkVerticalProps): JSX_2.Element; + hidden, +}: IconLinkVerticalProps): JSX_2.Element | null; // @public (undocumented) export type IconLinkVerticalClassKey = @@ -603,6 +604,7 @@ 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 a15e600e8c..817b7ad5c4 100644 --- a/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.tsx +++ b/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.tsx @@ -29,6 +29,7 @@ export type IconLinkVerticalProps = { label: string; onClick?: MouseEventHandler; title?: string; + hidden?: boolean; }; /** @public */ @@ -75,9 +76,14 @@ export function IconLinkVertical({ label, onClick, title, + hidden, }: IconLinkVerticalProps) { const classes = useIconStyles(); + if (hidden) { + return null; + } + if (disabled) { return ( diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 46d3030140..cdcfe99c77 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -9,6 +9,7 @@ import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { IconLinkVerticalProps } from '@backstage/core-components'; import { JsonValue } from '@backstage/types'; import { JSX as JSX_2 } from 'react'; import { ReactNode } from 'react'; @@ -390,6 +391,44 @@ export const EntityHeaderBlueprint: ExtensionBlueprint<{ }; }>; +// @alpha (undocumented) +export const EntityIconLinkBlueprint: ExtensionBlueprint<{ + kind: 'entity-icon-link'; + name: undefined; + params: { + props: IconLinkVerticalProps | (() => IconLinkVerticalProps); + }; + output: 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; + }; + configInput: { + color?: 'primary' | 'secondary' | undefined; + hidden?: boolean | undefined; + label?: string | undefined; + title?: string | undefined; + disabled?: boolean | undefined; + href?: string | undefined; + }; + dataRefs: { + props: ConfigurableExtensionDataRef< + () => IconLinkVerticalProps, + 'entity-icon-link-props', + {} + >; + }; +}>; + // @alpha (undocumented) export type EntityPredicate = | EntityPredicateExpression diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx new file mode 100644 index 0000000000..01e49d5d16 --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx @@ -0,0 +1,71 @@ +/* + * 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 { IconLinkVerticalProps } from '@backstage/core-components'; +import { + createExtensionBlueprint, + createExtensionDataRef, +} from '@backstage/frontend-plugin-api'; + +const entityIconLinkPropsDataRef = createExtensionDataRef< + () => IconLinkVerticalProps +>().with({ + id: 'entity-icon-link-props', +}); + +/** @alpha */ +export const EntityIconLinkBlueprint = createExtensionBlueprint({ + kind: 'entity-icon-link', + attachTo: { id: 'entity-card:catalog/about', input: 'iconLinks' }, + output: [entityIconLinkPropsDataRef], + dataRefs: { + props: entityIconLinkPropsDataRef, + }, + 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(), + }, + }, + *factory( + params: { + props: IconLinkVerticalProps | (() => IconLinkVerticalProps); + }, + { config }, + ) { + yield entityIconLinkPropsDataRef(() => ({ + ...(typeof params.props === 'function' ? params.props() : params.props), + ...Object.entries(config).reduce((rest, [key, value]) => { + // Only include properties that are defined in the config + // to avoid overriding defaults with undefined values + if (value !== undefined) { + 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; + }, {}), + })); + }, +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/index.ts b/plugins/catalog-react/src/alpha/blueprints/index.ts index b21522331d..640ebb881c 100644 --- a/plugins/catalog-react/src/alpha/blueprints/index.ts +++ b/plugins/catalog-react/src/alpha/blueprints/index.ts @@ -28,3 +28,4 @@ export { type EntityContextMenuItemParams, type UseProps, } from './EntityContextMenuItemBlueprint'; +export { EntityIconLinkBlueprint } from './EntityIconLinkBlueprint'; diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index 7ef20d8228..ca501cb107 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -18,6 +18,7 @@ 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'; @@ -336,8 +337,6 @@ const _default: FrontendPlugin< }; }>; 'entity-card:catalog/about': ExtensionDefinition<{ - kind: 'entity-card'; - name: 'about'; config: { filter: EntityPredicate | undefined; type: 'content' | 'summary' | 'info' | undefined; @@ -369,7 +368,21 @@ const _default: FrontendPlugin< optional: true; } >; - inputs: {}; + inputs: { + iconLinks: ExtensionInput< + ConfigurableExtensionDataRef< + () => IconLinkVerticalProps, + 'entity-icon-link-props', + {} + >, + { + singleton: false; + optional: false; + } + >; + }; + kind: 'entity-card'; + name: 'about'; params: { loader: () => Promise; filter?: string | EntityPredicate | ((entity: Entity) => boolean); @@ -923,6 +936,93 @@ const _default: FrontendPlugin< inputs: {}; params: EntityContextMenuItemParams; }>; + 'entity-icon-link:catalog/catalog-view-source': ExtensionDefinition<{ + kind: 'entity-icon-link'; + name: 'catalog-view-source'; + 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/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); + }; + }>; 'nav-item:catalog': ExtensionDefinition<{ kind: 'nav-item'; name: undefined; diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index 10e120d208..556b540b92 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -42,6 +42,8 @@ import { UserListFilterKind } from '@backstage/plugin-catalog-react'; // @public export interface AboutCardProps { + // (undocumented) + subheader?: JSX.Element; // (undocumented) variant?: InfoCardVariants; } diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index 8662518d30..775959bef0 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -14,17 +14,37 @@ * limitations under the License. */ -import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha'; +import { + EntityIconLinkBlueprint, + EntityCardBlueprint, +} 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'; -export const catalogAboutEntityCard = EntityCardBlueprint.make({ +export const catalogAboutEntityCard = EntityCardBlueprint.makeWithOverrides({ name: 'about', - params: { - type: 'info', - loader: async () => - import('../components/AboutCard').then(m => - compatWrapper(), - ), + inputs: { + iconLinks: createExtensionInput([EntityIconLinkBlueprint.dataRefs.props]), + }, + factory(originalFactory, { inputs }) { + function Subheader() { + // The props input 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 ; + } + return originalFactory({ + type: 'info', + async loader() { + const { AboutCard } = await import('../components/AboutCard'); + return compatWrapper( + } />, + ); + }, + }); }, }); diff --git a/plugins/catalog/src/alpha/entityIconLinks.tsx b/plugins/catalog/src/alpha/entityIconLinks.tsx new file mode 100644 index 0000000000..21595ad258 --- /dev/null +++ b/plugins/catalog/src/alpha/entityIconLinks.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EntityIconLinkBlueprint } from '@backstage/plugin-catalog-react/alpha'; +import { + useCatalogViewSourceEntityIconLinkProps, + useScaffolderLaunchTemplateEntityIconLinkProps, + useTechdocsViewDocumentionIconLinkProps, +} from '../components/AboutCard/AboutCard'; + +const catalogViewSourceEntityIconLink = EntityIconLinkBlueprint.make({ + name: 'catalog-view-source', + params: { + props: useCatalogViewSourceEntityIconLinkProps, + }, +}); + +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, +]; diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index c5dad31cf0..ff26d20003 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -33,6 +33,7 @@ import filters from './filters'; import navItems from './navItems'; import entityCards from './entityCards'; import entityContents from './entityContents'; +import entityIconLinks from './entityIconLinks'; import searchResultItems from './searchResultItems'; import contextMenuItems from './contextMenuItems'; @@ -57,6 +58,7 @@ export default createFrontendPlugin({ ...navItems, ...entityCards, ...entityContents, + ...entityIconLinks, ...contextMenuItems, ...searchResultItems, ], diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 88933b2a66..1c093a049c 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -28,7 +28,6 @@ import { makeStyles } from '@material-ui/core/styles'; import { AppIcon, HeaderIconLinkRow, - IconLinkVerticalProps, InfoCardVariants, Link, } from '@backstage/core-components'; @@ -70,6 +69,63 @@ import { TECHDOCS_EXTERNAL_ANNOTATION, } from '@backstage/plugin-techdocs-common'; +export function useCatalogViewSourceEntityIconLinkProps() { + const { entity } = useEntity(); + const scmIntegrationsApi = useApi(scmIntegrationsApiRef); + const { t } = useTranslationRef(catalogTranslationRef); + const entitySourceLocation = getEntitySourceLocation( + entity, + scmIntegrationsApi, + ); + return { + label: t('aboutCard.viewSource'), + disabled: !entitySourceLocation, + icon: , + href: entitySourceLocation?.locationTargetUrl, + }; +} + +export function useTechdocsViewDocumentionIconLinkProps() { + 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, + }); + + 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 useStyles = makeStyles({ gridItemCard: { display: 'flex', @@ -97,6 +153,7 @@ const useStyles = makeStyles({ */ export interface AboutCardProps { variant?: InfoCardVariants; + subheader?: JSX.Element; } /** @@ -107,15 +164,12 @@ export interface AboutCardProps { * card in your own repository instead, that is perfect for your own needs. */ export function AboutCard(props: AboutCardProps) { - const { variant } = props; - const app = useApp(); + const { variant, subheader } = props; const classes = useStyles(); const { entity } = useEntity(); - const scmIntegrationsApi = useApi(scmIntegrationsApiRef); const catalogApi = useApi(catalogApiRef); const alertApi = useApi(alertApiRef); const errorApi = useApi(errorApiRef); - const viewTechdocLink = useRouteRef(viewTechDocRouteRef); const templateRoute = useRouteRef(createFromTemplateRouteRef); const sourceTemplateRef = useSourceTemplateCompoundEntityRef(entity); const { allowed: canRefresh } = useEntityPermission( @@ -123,53 +177,14 @@ export function AboutCard(props: AboutCardProps) { ); const { t } = useTranslationRef(catalogTranslationRef); - const { allowed: canCreateTemplateTask } = usePermission({ - permission: taskCreatePermission, - }); - - const entitySourceLocation = getEntitySourceLocation( - entity, - scmIntegrationsApi, - ); const entityMetadataEditUrl = entity.metadata.annotations?.[ANNOTATION_EDIT_URL]; - const viewInSource: IconLinkVerticalProps = { - label: t('aboutCard.viewSource'), - disabled: !entitySourceLocation, - icon: , - href: entitySourceLocation?.locationTargetUrl, - }; - const viewInTechDocs: IconLinkVerticalProps = { - label: t('aboutCard.viewTechdocs'), - disabled: - !( - entity.metadata.annotations?.[TECHDOCS_ANNOTATION] || - entity.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION] - ) || !viewTechdocLink, - icon: , - href: buildTechDocsURL(entity, viewTechdocLink), - }; - - const subHeaderLinks = [viewInSource, viewInTechDocs]; - - if (isTemplateEntityV1beta3(entity)) { - const Icon = app.getSystemIcon('scaffolder') ?? CreateComponentIcon; - - const launchTemplate: IconLinkVerticalProps = { - label: t('aboutCard.launchTemplate'), - icon: , - disabled: !templateRoute || !canCreateTemplateTask, - href: - templateRoute && - templateRoute({ - templateName: entity.metadata.name, - namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, - }), - }; - - subHeaderLinks.push(launchTemplate); - } + const viewCatalogSourceIconLink = useCatalogViewSourceEntityIconLinkProps(); + const viewTechdocsDocumentationIconLink = + useTechdocsViewDocumentionIconLinkProps(); + const launchScaffolderTemplateIconLink = + useScaffolderLaunchTemplateEntityIconLinkProps(); let cardClass = ''; if (variant === 'gridItem') { @@ -240,7 +255,17 @@ export function AboutCard(props: AboutCardProps) { )} } - subheader={} + subheader={ + subheader ?? ( + + ) + } /> From 3c59ece2e0c32317799a0b3377c6af0521f98514 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 27 May 2025 08:39:59 +0200 Subject: [PATCH 2/3] refactor: apply review suggestions Signed-off-by: Camila Belo --- .changeset/fruity-peaches-march.md | 5 - .changeset/large-baboons-prove.md | 4 + .changeset/large-crabs-joke.md | 6 + .changeset/loose-ants-learn.md | 5 + .changeset/open-eyes-learn.md | 48 ++----- .changeset/petite-pots-lead.md | 6 + .changeset/rare-rules-move.md | 5 + packages/core-components/report.api.md | 4 +- .../HeaderIconLinkRow/IconLinkVertical.tsx | 6 - plugins/catalog-import/report-alpha.api.md | 4 +- plugins/catalog-react/report-alpha.api.md | 38 +++--- .../blueprints/EntityIconLinkBlueprint.tsx | 39 ++++-- plugins/catalog/package.json | 1 + plugins/catalog/report-alpha.api.md | 110 +++++----------- plugins/catalog/report.api.md | 9 +- plugins/catalog/src/alpha/entityCards.tsx | 35 +++-- plugins/catalog/src/alpha/entityIconLinks.tsx | 30 +---- .../src/components/AboutCard/AboutCard.tsx | 120 ++++++------------ plugins/scaffolder-react/report-alpha.api.md | 14 +- plugins/scaffolder-react/src/alpha.ts | 2 + .../useScaffolderTemplateIconLinkProps.tsx | 59 +++++++++ plugins/scaffolder/report-alpha.api.md | 40 ++++++ plugins/scaffolder/src/alpha/plugin.tsx | 25 +++- plugins/scaffolder/src/routes.ts | 7 + plugins/scaffolder/src/translation.ts | 3 + plugins/techdocs-react/package.json | 2 + plugins/techdocs-react/report-alpha.api.md | 14 ++ plugins/techdocs-react/src/alpha.ts | 2 + .../src/{hooks.ts => hooks.tsx} | 38 ++++++ plugins/techdocs/report-alpha.api.md | 51 +++++++- plugins/techdocs/src/alpha.tsx | 32 ++++- plugins/techdocs/src/routes.ts | 12 +- plugins/techdocs/src/translation.ts | 27 ++++ yarn.lock | 3 + 34 files changed, 523 insertions(+), 283 deletions(-) delete mode 100644 .changeset/fruity-peaches-march.md create mode 100644 .changeset/large-crabs-joke.md create mode 100644 .changeset/loose-ants-learn.md create mode 100644 .changeset/petite-pots-lead.md create mode 100644 .changeset/rare-rules-move.md create mode 100644 plugins/scaffolder-react/src/hooks/useScaffolderTemplateIconLinkProps.tsx rename plugins/techdocs-react/src/{hooks.ts => hooks.tsx} (76%) create mode 100644 plugins/techdocs/src/translation.ts 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" From 8ee146cea9f26f4a43f8fae0621761a85a01d7fe Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 3 Jun 2025 09:07:32 +0200 Subject: [PATCH 3/3] refactor: apply second round of review suggestions Signed-off-by: Camila Belo --- .changeset/large-baboons-prove.md | 4 +- .changeset/large-crabs-joke.md | 6 - .changeset/loose-ants-learn.md | 1 + .changeset/petite-pots-lead.md | 6 - .changeset/rare-rules-move.md | 1 + .../search/declarative-integration.md | 2 +- plugins/catalog-import/report-alpha.api.md | 4 +- plugins/catalog-react/report-alpha.api.md | 12 ++ .../blueprints/EntityIconLinkBlueprint.tsx | 61 ++++---- plugins/catalog/package.json | 1 - plugins/catalog/report-alpha.api.md | 14 ++ plugins/catalog/report.api.md | 6 +- plugins/catalog/src/alpha/entityCards.tsx | 11 +- .../src/components/AboutCard/AboutCard.tsx | 133 +++++++++++++----- plugins/scaffolder-react/report-alpha.api.md | 14 +- plugins/scaffolder-react/src/alpha.ts | 2 - plugins/scaffolder/report-alpha.api.md | 11 +- .../useScaffolderTemplateIconLinkProps.tsx | 26 ++-- plugins/scaffolder/src/alpha/plugin.tsx | 16 +-- plugins/scaffolder/src/routes.ts | 7 - plugins/techdocs-react/package.json | 2 - plugins/techdocs-react/report-alpha.api.md | 14 -- plugins/techdocs-react/src/alpha.ts | 2 - .../src/{hooks.tsx => hooks.ts} | 38 ----- plugins/techdocs/package.json | 4 +- plugins/techdocs/report-alpha.api.md | 25 ++-- .../hooks/useTechdocsReaderIconLinkProps.tsx | 51 +++++++ .../src/{alpha.tsx => alpha/index.tsx} | 36 ++--- plugins/techdocs/src/routes.ts | 12 +- yarn.lock | 3 - 30 files changed, 260 insertions(+), 265 deletions(-) delete mode 100644 .changeset/large-crabs-joke.md delete mode 100644 .changeset/petite-pots-lead.md rename plugins/{scaffolder-react/src => scaffolder/src/alpha}/hooks/useScaffolderTemplateIconLinkProps.tsx (71%) rename plugins/techdocs-react/src/{hooks.tsx => hooks.ts} (76%) create mode 100644 plugins/techdocs/src/alpha/hooks/useTechdocsReaderIconLinkProps.tsx rename plugins/techdocs/src/{alpha.tsx => alpha/index.tsx} (87%) diff --git a/.changeset/large-baboons-prove.md b/.changeset/large-baboons-prove.md index 18949dd976..072d57de4c 100644 --- a/.changeset/large-baboons-prove.md +++ b/.changeset/large-baboons-prove.md @@ -4,6 +4,6 @@ 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** +**BREAKING ALPHA** -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`). +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. It means that they will not be available unless you install the `TechDocs` and `Scaffolder` plugins. Also If you are using translation for these icon 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 deleted file mode 100644 index c8e021d09c..0000000000 --- a/.changeset/large-crabs-joke.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@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 index 728bdc8942..29f357966c 100644 --- a/.changeset/loose-ants-learn.md +++ b/.changeset/loose-ants-learn.md @@ -2,4 +2,5 @@ '@backstage/plugin-techdocs': minor --- +**New Frontend System Only:** 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/petite-pots-lead.md b/.changeset/petite-pots-lead.md deleted file mode 100644 index 7ce331f5b5..0000000000 --- a/.changeset/petite-pots-lead.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@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 index 9786bc1104..6abc778941 100644 --- a/.changeset/rare-rules-move.md +++ b/.changeset/rare-rules-move.md @@ -2,4 +2,5 @@ '@backstage/plugin-scaffolder': minor --- +**New Frontend System Only:** The `Scaffolder` plugin is now responsible for providing an entity icon link extension to launch templates from the catalog entity page. diff --git a/docs/features/search/declarative-integration.md b/docs/features/search/declarative-integration.md index c331d810ef..b8f1c6ce5c 100644 --- a/docs/features/search/declarative-integration.md +++ b/docs/features/search/declarative-integration.md @@ -182,7 +182,7 @@ export default createPlugin({ }) ``` -Here is the `plugins/techdocs/alpha.tsx` final version, and you can also take a look at the [actual implementation](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/alpha.tsx) of a custom `TechDocs` search result item: +Here is the `plugins/techdocs/alpha/index.tsx` final version, and you can also take a look at the [actual implementation](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/alpha/index.tsx) of a custom `TechDocs` search result item: ```tsx // plugins/techdocs/alpha.tsx diff --git a/plugins/catalog-import/report-alpha.api.md b/plugins/catalog-import/report-alpha.api.md index 90716bc442..d4dab4549a 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.nextButtonText': 'Analyze'; readonly 'stepInitAnalyzeUrl.urlHelperText': 'Enter the full path to your entity file to start tracking your component'; + readonly 'stepInitAnalyzeUrl.nextButtonText': 'Analyze'; 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 e4be5ae299..36a93a8a5e 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -407,6 +407,13 @@ export const EntityIconLinkBlueprint: ExtensionBlueprint<{ optional: true; } > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > | ConfigurableExtensionDataRef< () => IconLinkVerticalProps, 'entity-icon-link-props', @@ -434,6 +441,11 @@ export const EntityIconLinkBlueprint: ExtensionBlueprint<{ 'catalog.entity-filter-function', {} >; + filterExpression: ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + {} + >; }; }>; diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx index fce281b70f..11c9a2b805 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityIconLinkBlueprint.tsx @@ -20,14 +20,15 @@ import { createExtensionDataRef, } from '@backstage/frontend-plugin-api'; -import { - EntityPredicate, - entityPredicateToFilterFunction, -} from '../predicates'; +import { EntityPredicate } from '../predicates'; import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema'; -import { entityFilterFunctionDataRef } from './extensionData'; +import { + entityFilterExpressionDataRef, + entityFilterFunctionDataRef, +} from './extensionData'; import { Entity } from '@backstage/catalog-model'; +import { resolveEntityFilterData } from './resolveEntityFilterData'; const entityIconLinkPropsDataRef = createExtensionDataRef< () => IconLinkVerticalProps @@ -39,10 +40,15 @@ const entityIconLinkPropsDataRef = createExtensionDataRef< export const EntityIconLinkBlueprint = createExtensionBlueprint({ kind: 'entity-icon-link', attachTo: { id: 'entity-card:catalog/about', input: 'iconLinks' }, - output: [entityIconLinkPropsDataRef, entityFilterFunctionDataRef.optional()], + output: [ + entityFilterFunctionDataRef.optional(), + entityFilterExpressionDataRef.optional(), + entityIconLinkPropsDataRef, + ], dataRefs: { useProps: entityIconLinkPropsDataRef, filterFunction: entityFilterFunctionDataRef, + filterExpression: entityFilterExpressionDataRef, }, config: { schema: { @@ -56,33 +62,22 @@ export const EntityIconLinkBlueprint = createExtensionBlueprint({ useProps: () => Omit; filter?: EntityPredicate | ((entity: Entity) => boolean); }, - { config }, + { config, node }, ) { - yield entityIconLinkPropsDataRef(() => ({ - ...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 - if (value !== undefined) { - return { - ...rest, - [key]: value, - }; - } - 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), - ); - } + const { filter, useProps } = params; + yield* resolveEntityFilterData(filter, config, node); + // Only include properties that are defined in the config + // to avoid overriding defaults with undefined values + const configProps = Object.entries(config).reduce( + (rest, [key, value]) => + value !== undefined + ? { + ...rest, + [key]: value, + } + : rest, + {}, + ); + yield entityIconLinkPropsDataRef(() => ({ ...useProps(), ...configProps })); }, }); diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 29c4eb134b..1e78d40306 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -70,7 +70,6 @@ "@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 d7a7be0778..4be914a69d 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -377,6 +377,13 @@ const _default: FrontendPlugin< optional: true; } > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > | ConfigurableExtensionDataRef< () => IconLinkVerticalProps, 'entity-icon-link-props', @@ -964,6 +971,13 @@ const _default: FrontendPlugin< optional: true; } > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > | ConfigurableExtensionDataRef< () => IconLinkVerticalProps, 'entity-icon-link-props', diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index 1b9b4dfa8c..52b13e1c43 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -40,10 +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 type AboutCardProps = Pick; +export type AboutCardProps = { + variant?: InfoCardVariants; +}; // @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 6533bde3a1..3913f8b2b7 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -25,13 +25,15 @@ import { IconLinkVerticalProps, } from '@backstage/core-components'; import { useEntity } from '@backstage/plugin-catalog-react'; +import { buildFilterFn } from './filter/FilterWrapper'; export const catalogAboutEntityCard = EntityCardBlueprint.makeWithOverrides({ name: 'about', inputs: { iconLinks: createExtensionInput([ - EntityIconLinkBlueprint.dataRefs.useProps, EntityIconLinkBlueprint.dataRefs.filterFunction.optional(), + EntityIconLinkBlueprint.dataRefs.filterExpression.optional(), + EntityIconLinkBlueprint.dataRefs.useProps, ]), }, factory(originalFactory, { inputs }) { @@ -41,9 +43,10 @@ export const catalogAboutEntityCard = EntityCardBlueprint.makeWithOverrides({ // call them in a component function to avoid breaking the rules of hooks. const links = inputs.iconLinks.reduce((rest, iconLink) => { const props = iconLink.get(EntityIconLinkBlueprint.dataRefs.useProps)(); - const filter = - iconLink.get(EntityIconLinkBlueprint.dataRefs.filterFunction) ?? - (() => true); + const filter = buildFilterFn( + iconLink.get(EntityIconLinkBlueprint.dataRefs.filterFunction), + iconLink.get(EntityIconLinkBlueprint.dataRefs.filterExpression), + ); if (filter(entity)) { return [...rest, props]; } diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index e5d6bbeef2..fd1c05ba5f 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -13,52 +13,70 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - ANNOTATION_EDIT_URL, - ANNOTATION_LOCATION, - stringifyEntityRef, -} from '@backstage/catalog-model'; + +import { useCallback } from 'react'; + +import { makeStyles } from '@material-ui/core/styles'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; import Divider from '@material-ui/core/Divider'; import IconButton from '@material-ui/core/IconButton'; -import { makeStyles } from '@material-ui/core/styles'; +import CachedIcon from '@material-ui/icons/Cached'; +import EditIcon from '@material-ui/icons/Edit'; +import DocsIcon from '@material-ui/icons/Description'; +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; + import { AppIcon, HeaderIconLinkRow, + IconLinkVerticalProps, InfoCardVariants, Link, } from '@backstage/core-components'; -import { useCallback } from 'react'; +import { + alertApiRef, + errorApiRef, + useApp, + useApi, + useRouteRef, +} from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; + import { ScmIntegrationIcon, scmIntegrationsApiRef, } from '@backstage/integration-react'; + import { - alertApiRef, - errorApiRef, - useApi, - useRouteRef, -} from '@backstage/core-plugin-api'; + DEFAULT_NAMESPACE, + ANNOTATION_EDIT_URL, + ANNOTATION_LOCATION, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { catalogApiRef, getEntitySourceLocation, useEntity, } from '@backstage/plugin-catalog-react'; -import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; - -import { AboutContent } from './AboutContent'; -import CachedIcon from '@material-ui/icons/Cached'; -import EditIcon from '@material-ui/icons/Edit'; import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha'; import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha'; -import { useSourceTemplateCompoundEntityRef } from './hooks'; -import { catalogTranslationRef } from '../../alpha/translation'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { useTechdocsReaderIconLinkProps } from '@backstage/plugin-techdocs-react/alpha'; -import { useScaffolderTemplateIconLinkProps } from '@backstage/plugin-scaffolder-react/alpha'; + +import { + TECHDOCS_ANNOTATION, + TECHDOCS_EXTERNAL_ANNOTATION, +} from '@backstage/plugin-techdocs-common'; +import { buildTechDocsURL } from '@backstage/plugin-techdocs-react'; + import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha'; + +import { usePermission } from '@backstage/plugin-permission-react'; + +import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; +import { catalogTranslationRef } from '../../alpha/translation'; +import { useSourceTemplateCompoundEntityRef } from './hooks'; +import { AboutContent } from './AboutContent'; export function useCatalogSourceIconLinkProps() { const { entity } = useEntity(); @@ -76,17 +94,55 @@ export function useCatalogSourceIconLinkProps() { }; } +// TODO: This hook is duplicated from the TechDocs plugin for backwards compatibility +// Remove it when the the legacy frontend system support is dropped. +function useTechdocsReaderIconLinkProps(): IconLinkVerticalProps { + 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), + }; +} + +// TODO: This hook is duplicated from the Scaffolder plugin for backwards compatibility +// Remove it when the the legacy frontend system support is dropped. +function useScaffolderTemplateIconLinkProps(): IconLinkVerticalProps { + 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, + }); + + return { + label: t('aboutCard.launchTemplate'), + icon: , + disabled: !templateRoute || !canCreateTemplateTask, + href: + templateRoute && + templateRoute({ + templateName: entity.metadata.name, + namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, + }), + }; +} + function DefaultAboutCardSubheader() { const { entity } = useEntity(); const catalogSourceIconLink = useCatalogSourceIconLinkProps(); - const techdocsreaderIconLink = useTechdocsReaderIconLinkProps({ - translationRef: catalogTranslationRef, - externalRouteRef: viewTechDocRouteRef, - }); - const scaffolderTemplateIconLink = useScaffolderTemplateIconLinkProps({ - translationRef: catalogTranslationRef, - externalRouteRef: createFromTemplateRouteRef, - }); + const techdocsreaderIconLink = useTechdocsReaderIconLinkProps(); + const scaffolderTemplateIconLink = useScaffolderTemplateIconLinkProps(); const links = [catalogSourceIconLink, techdocsreaderIconLink]; if (isTemplateEntityV1beta3(entity)) { @@ -116,8 +172,16 @@ const useStyles = makeStyles({ }, }); -export interface InternalAboutCardProps { +/** + * Props for {@link EntityAboutCard}. + * + * @public + */ +export type AboutCardProps = { variant?: InfoCardVariants; +}; + +export interface InternalAboutCardProps extends AboutCardProps { subheader?: JSX.Element; } @@ -217,13 +281,6 @@ export function InternalAboutCard(props: InternalAboutCardProps) { ); } -/** - * Props for {@link EntityAboutCard}. - * - * @public - */ -export type AboutCardProps = Pick; - /** * Exported publicly via the EntityAboutCard * diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index 3a1e330614..2014bff9be 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -14,7 +14,6 @@ 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'; @@ -345,8 +344,8 @@ export const scaffolderReactTranslationRef: TranslationRef< readonly 'stepper.backButtonText': 'Back'; readonly 'stepper.createButtonText': 'Create'; readonly 'stepper.reviewButtonText': 'Review'; - readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; readonly 'stepper.nextButtonText': 'Next'; + readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; readonly 'templateCategoryPicker.title': 'Categories'; readonly 'templateCard.noDescription': 'No description'; readonly 'templateCard.chooseButtonText': 'Choose'; @@ -485,17 +484,6 @@ 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 1b4935c522..e18c758b3d 100644 --- a/plugins/scaffolder-react/src/alpha.ts +++ b/plugins/scaffolder-react/src/alpha.ts @@ -17,5 +17,3 @@ export * from './next'; export { scaffolderReactTranslationRef } from './translation'; - -export { useScaffolderTemplateIconLinkProps } from './hooks/useScaffolderTemplateIconLinkProps'; diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index 05e94a446a..54acadd561 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -48,10 +48,6 @@ const _default: FrontendPlugin< }, { registerComponent: ExternalRouteRef; - createFromTemplate: ExternalRouteRef<{ - namespace: string; - templateName: string; - }>; viewTechDoc: ExternalRouteRef<{ name: string; kind: string; @@ -149,6 +145,13 @@ const _default: FrontendPlugin< optional: true; } > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > | ConfigurableExtensionDataRef< () => IconLinkVerticalProps, 'entity-icon-link-props', diff --git a/plugins/scaffolder-react/src/hooks/useScaffolderTemplateIconLinkProps.tsx b/plugins/scaffolder/src/alpha/hooks/useScaffolderTemplateIconLinkProps.tsx similarity index 71% rename from plugins/scaffolder-react/src/hooks/useScaffolderTemplateIconLinkProps.tsx rename to plugins/scaffolder/src/alpha/hooks/useScaffolderTemplateIconLinkProps.tsx index b59f858ce9..87754468fa 100644 --- a/plugins/scaffolder-react/src/hooks/useScaffolderTemplateIconLinkProps.tsx +++ b/plugins/scaffolder/src/alpha/hooks/useScaffolderTemplateIconLinkProps.tsx @@ -15,31 +15,25 @@ */ import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; -import { - ExternalRouteRef, - useApp, - useRouteRef, -} from '@backstage/core-plugin-api'; +import { 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'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { scaffolderTranslationRef } from '../../translation'; +import { selectedTemplateRouteRef } from '../../routes'; + +// Note: If you update this hook, please also update the "useScaffolderTemplateIconLinkProps" hook +// in the "plugins/catalog/src/components/AboutCard/AboutCard.tsx" file /** @alpha */ -export function useScaffolderTemplateIconLinkProps(options: { - translationRef: TranslationRef; - externalRouteRef: ExternalRouteRef; -}) { - const { translationRef, externalRouteRef } = options; +export function useScaffolderTemplateIconLinkProps() { const app = useApp(); const { entity } = useEntity(); - const templateRoute = useRouteRef(externalRouteRef); - const { t } = useTranslationRef(translationRef); + const templateRoute = useRouteRef(selectedTemplateRouteRef); + const { t } = useTranslationRef(scaffolderTranslationRef); const Icon = app.getSystemIcon('scaffolder') ?? CreateComponentIcon; const { allowed: canCreateTemplateTask } = usePermission({ permission: taskCreatePermission, diff --git a/plugins/scaffolder/src/alpha/plugin.tsx b/plugins/scaffolder/src/alpha/plugin.tsx index 56f7343978..a414c61da5 100644 --- a/plugins/scaffolder/src/alpha/plugin.tsx +++ b/plugins/scaffolder/src/alpha/plugin.tsx @@ -26,7 +26,6 @@ import { selectedTemplateRouteRef, templatingExtensionsRouteRef, viewTechDocRouteRef, - createFromTemplateRouteRef, } from '../routes'; import { repoUrlPickerFormField, @@ -35,25 +34,17 @@ import { scaffolderApi, } from './extensions'; import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { - formFieldsApi, - useScaffolderTemplateIconLinkProps, -} from '@backstage/plugin-scaffolder-react/alpha'; +import { formFieldsApi } from '@backstage/plugin-scaffolder-react/alpha'; import { formDecoratorsApi } from './api'; import { EntityIconLinkBlueprint } from '@backstage/plugin-catalog-react/alpha'; -import { scaffolderTranslationRef } from '../translation'; +import { useScaffolderTemplateIconLinkProps } from './hooks/useScaffolderTemplateIconLinkProps'; /** @alpha */ const scaffolderEntityIconLink = EntityIconLinkBlueprint.make({ name: 'launch-template', params: { filter: isTemplateEntityV1beta3, - useProps: () => { - return useScaffolderTemplateIconLinkProps({ - translationRef: scaffolderTranslationRef, - externalRouteRef: createFromTemplateRouteRef, - }); - }, + useProps: useScaffolderTemplateIconLinkProps, }, }); @@ -72,7 +63,6 @@ export default createFrontendPlugin({ }), externalRoutes: convertLegacyRouteRefs({ registerComponent: registerComponentRouteRef, - createFromTemplate: createFromTemplateRouteRef, viewTechDoc: viewTechDocRouteRef, }), extensions: [ diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index 0a36089928..2e336757b8 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -32,13 +32,6 @@ 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/techdocs-react/package.json b/plugins/techdocs-react/package.json index 774b3e4dae..618af87698 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -63,11 +63,9 @@ "@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 f4f4e612ed..d916d1fcb0 100644 --- a/plugins/techdocs-react/report-alpha.api.md +++ b/plugins/techdocs-react/report-alpha.api.md @@ -6,9 +6,6 @@ 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<{ @@ -62,16 +59,5 @@ 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 2cf7878047..83c0444753 100644 --- a/plugins/techdocs-react/src/alpha.ts +++ b/plugins/techdocs-react/src/alpha.ts @@ -26,8 +26,6 @@ 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.tsx b/plugins/techdocs-react/src/hooks.ts similarity index 76% rename from plugins/techdocs-react/src/hooks.tsx rename to plugins/techdocs-react/src/hooks.ts index b8819a345e..1203622cb2 100644 --- a/plugins/techdocs-react/src/hooks.tsx +++ b/plugins/techdocs-react/src/hooks.ts @@ -15,45 +15,7 @@ */ 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/package.json b/plugins/techdocs/package.json index f688fb9503..c79dadca05 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -30,7 +30,7 @@ "sideEffects": false, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.tsx", + "./alpha": "./src/alpha/index.tsx", "./package.json": "./package.json" }, "main": "src/index.ts", @@ -38,7 +38,7 @@ "typesVersions": { "*": { "alpha": [ - "src/alpha.tsx" + "src/alpha/index.tsx" ], "package.json": [ "package.json" diff --git a/plugins/techdocs/report-alpha.api.md b/plugins/techdocs/report-alpha.api.md index 69e98a4c33..ce1d10a00c 100644 --- a/plugins/techdocs/report-alpha.api.md +++ b/plugins/techdocs/report-alpha.api.md @@ -12,7 +12,6 @@ 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'; @@ -22,7 +21,6 @@ import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-rea 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< @@ -35,13 +33,7 @@ const _default: FrontendPlugin< }>; entityContent: RouteRef; }, - { - viewTechDoc: ExternalRouteRef<{ - name: string; - kind: string; - namespace: string; - }>; - }, + {}, { 'api:techdocs': ExtensionDefinition<{ kind: 'api'; @@ -203,6 +195,13 @@ const _default: FrontendPlugin< optional: true; } > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > | ConfigurableExtensionDataRef< () => IconLinkVerticalProps, 'entity-icon-link-props', @@ -381,13 +380,5 @@ 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/hooks/useTechdocsReaderIconLinkProps.tsx b/plugins/techdocs/src/alpha/hooks/useTechdocsReaderIconLinkProps.tsx new file mode 100644 index 0000000000..0cd2cf9ca1 --- /dev/null +++ b/plugins/techdocs/src/alpha/hooks/useTechdocsReaderIconLinkProps.tsx @@ -0,0 +1,51 @@ +/* + * 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 DocsIcon from '@material-ui/icons/Description'; + +import { useRouteRef } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; + +import { + TECHDOCS_ANNOTATION, + TECHDOCS_EXTERNAL_ANNOTATION, +} from '@backstage/plugin-techdocs-common'; +import { buildTechDocsURL } from '@backstage/plugin-techdocs-react'; + +import { useEntity } from '@backstage/plugin-catalog-react'; + +import { techdocsTranslationRef } from '../../translation'; +import { rootDocsRouteRef } from '../../routes'; + +// Note: If you update this hook, please also update the "useTechdocsReaderIconLinkProps" hook +// in the "plugins/catalog/src/components/AboutCard/AboutCard.tsx" file +/** @alpha */ +export function useTechdocsReaderIconLinkProps() { + const { entity } = useEntity(); + const viewTechdocLink = useRouteRef(rootDocsRouteRef); + const { t } = useTranslationRef(techdocsTranslationRef); + + return { + label: t('aboutCard.viewTechdocs'), + disabled: + !( + entity.metadata.annotations?.[TECHDOCS_ANNOTATION] || + entity.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION] + ) || !viewTechdocLink, + icon: , + href: buildTechDocsURL(entity, viewTechdocLink), + }; +} diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha/index.tsx similarity index 87% rename from plugins/techdocs/src/alpha.tsx rename to plugins/techdocs/src/alpha/index.tsx index 37cdb64ead..44205a480a 100644 --- a/plugins/techdocs/src/alpha.tsx +++ b/plugins/techdocs/src/alpha/index.tsx @@ -41,38 +41,27 @@ import { } 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'; +import { TechDocsClient, TechDocsStorageClient } from '../client'; import { rootCatalogDocsRouteRef, rootDocsRouteRef, rootRouteRef, - viewTechDocRouteRef, -} from './routes'; -import { TechDocsReaderLayout } from './reader'; -import { - attachTechDocsAddonComponentData, - useTechdocsReaderIconLinkProps, -} from '@backstage/plugin-techdocs-react/alpha'; +} from '../routes'; +import { TechDocsReaderLayout } from '../reader'; +import { attachTechDocsAddonComponentData } from '@backstage/plugin-techdocs-react/alpha'; import { TechDocsAddons, techdocsApiRef, techdocsStorageApiRef, } from '@backstage/plugin-techdocs-react'; -import { techdocsTranslationRef } from './translation'; - -export { techdocsTranslationRef } from './translation'; +import { useTechdocsReaderIconLinkProps } from './hooks/useTechdocsReaderIconLinkProps'; /** @alpha */ const techdocsEntityIconLink = EntityIconLinkBlueprint.make({ name: 'read-docs', params: { - useProps: () => { - return useTechdocsReaderIconLinkProps({ - translationRef: techdocsTranslationRef, - externalRouteRef: viewTechDocRouteRef, - }); - }, + useProps: useTechdocsReaderIconLinkProps, }, }); @@ -133,7 +122,7 @@ export const techDocsSearchResultListItemExtension = predicate: result => result.type === 'techdocs', component: async () => { const { TechDocsSearchResultListItem } = await import( - './search/components/TechDocsSearchResultListItem' + '../search/components/TechDocsSearchResultListItem' ); return props => compatWrapper( @@ -154,7 +143,7 @@ const techDocsPage = PageBlueprint.make({ defaultPath: '/docs', routeRef: convertLegacyRouteRef(rootRouteRef), loader: () => - import('./home/components/TechDocsIndexPage').then(m => + import('../home/components/TechDocsIndexPage').then(m => compatWrapper(), ), }, @@ -182,7 +171,7 @@ const techDocsReaderPage = PageBlueprint.makeWithOverrides({ defaultPath: '/docs/:namespace/:kind/:name', routeRef: convertLegacyRouteRef(rootDocsRouteRef), loader: async () => - await import('./Router').then(({ TechDocsReaderRouter }) => { + await import('../Router').then(({ TechDocsReaderRouter }) => { return compatWrapper( @@ -217,7 +206,7 @@ const techDocsEntityContent = EntityContentBlueprint.makeWithOverrides({ defaultTitle: 'TechDocs', routeRef: convertLegacyRouteRef(rootCatalogDocsRouteRef), loader: () => - import('./Router').then(({ EmbeddedDocsRouter }) => { + import('../Router').then(({ EmbeddedDocsRouter }) => { const addons = context.inputs.addons.map(output => { const options = output.get(AddonBlueprint.dataRefs.addon); const Addon = options.component; @@ -260,7 +249,7 @@ const techDocsNavItem = NavItemBlueprint.make({ /** @alpha */ export default createFrontendPlugin({ pluginId: 'techdocs', - info: { packageJson: () => import('../package.json') }, + info: { packageJson: () => import('../../package.json') }, extensions: [ techDocsClientApi, techDocsStorageApi, @@ -277,7 +266,4 @@ 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 69f76a1627..e781550899 100644 --- a/plugins/techdocs/src/routes.ts +++ b/plugins/techdocs/src/routes.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - createRouteRef, - createExternalRouteRef, -} from '@backstage/core-plugin-api'; +import { createRouteRef } from '@backstage/core-plugin-api'; export const rootRouteRef = createRouteRef({ id: 'techdocs:index-page', @@ -31,10 +28,3 @@ 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/yarn.lock b/yarn.lock index a7ca1e5817..ab61b20c89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6378,7 +6378,6 @@ __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:^" @@ -8532,13 +8531,11 @@ __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"