refactor: apply review suggestions

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2025-05-27 08:39:59 +02:00
parent 406acb6d61
commit 3c59ece2e0
34 changed files with 523 additions and 283 deletions
-5
View File
@@ -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.
+4
View File
@@ -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`).
+6
View File
@@ -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.
+5
View File
@@ -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.
+13 -35
View File
@@ -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: <MyIconLinkIcon />,
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.
+6
View File
@@ -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.
+5
View File
@@ -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.
+1 -3
View File
@@ -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<HTMLAnchorElement>;
title?: string;
hidden?: boolean;
};
// @public (undocumented)
@@ -29,7 +29,6 @@ export type IconLinkVerticalProps = {
label: string;
onClick?: MouseEventHandler<HTMLAnchorElement>;
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 (
<Box title={title} className={classnames(classes.link, classes.disabled)}>
+2 -2
View File
@@ -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';
+23 -15
View File
@@ -396,36 +396,44 @@ export const EntityIconLinkBlueprint: ExtensionBlueprint<{
kind: 'entity-icon-link';
name: undefined;
params: {
props: IconLinkVerticalProps | (() => IconLinkVerticalProps);
useProps: () => Omit<IconLinkVerticalProps, 'color'>;
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',
{}
>;
};
}>;
@@ -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<IconLinkVerticalProps, 'color'>;
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),
);
}
},
});
+1
View File
@@ -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:^",
+31 -79
View File
@@ -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<IconLinkVerticalProps, 'color'>;
filter?: EntityPredicate | ((entity: Entity) => boolean);
};
}>;
'nav-item:catalog': ExtensionDefinition<{
+3 -6
View File
@@ -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<InternalAboutCardProps, 'variant'>;
// @public (undocumented)
export function AboutContent(props: AboutContentProps): JSX_2.Element;
+26 -9
View File
@@ -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 <HeaderIconLinkRow links={links} />;
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<IconLinkVerticalProps>());
return links.length ? <HeaderIconLinkRow links={links} /> : null;
}
return originalFactory({
type: 'info',
async loader() {
const { AboutCard } = await import('../components/AboutCard');
const { InternalAboutCard } = await import(
'../components/AboutCard/AboutCard'
);
return compatWrapper(
<AboutCard variant="gridItem" subheader={<Subheader />} />,
<InternalAboutCard variant="gridItem" subheader={<Subheader />} />,
);
},
});
+4 -26
View File
@@ -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];
@@ -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: <DocsIcon />,
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: <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 <HeaderIconLinkRow links={links} />;
}
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 ?? (
<HeaderIconLinkRow
links={[
viewCatalogSourceIconLink,
viewTechdocsDocumentationIconLink,
launchScaffolderTemplateIconLink,
]}
/>
)
}
subheader={subheader ?? <DefaultAboutCardSubheader />}
/>
<Divider />
<CardContent className={cardContentClass}>
@@ -274,3 +216,21 @@ export function AboutCard(props: AboutCardProps) {
</Card>
);
}
/**
* Props for {@link EntityAboutCard}.
*
* @public
*/
export type AboutCardProps = Pick<InternalAboutCardProps, 'variant'>;
/**
* 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 <InternalAboutCard {...props} />;
}
+13 -1
View File
@@ -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<string, JsonValue>,
) => [Record<string, any>, Dispatch<SetStateAction<Record<string, any>>>];
// @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;
+2
View File
@@ -17,3 +17,5 @@
export * from './next';
export { scaffolderReactTranslationRef } from './translation';
export { useScaffolderTemplateIconLinkProps } from './hooks/useScaffolderTemplateIconLinkProps';
@@ -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: <Icon />,
disabled: !templateRoute || !canCreateTemplateTask,
href:
templateRoute &&
templateRoute({
templateName: entity.metadata.name,
namespace: entity.metadata.namespace || DEFAULT_NAMESPACE,
}),
};
}
+40
View File
@@ -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<undefined>;
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<IconLinkVerticalProps, 'color'>;
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';
+24 -1
View File
@@ -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,
+7
View File
@@ -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
*/
+3
View File
@@ -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',
+2
View File
@@ -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",
@@ -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<TAddonProps = {}> = {
component: ComponentType<TAddonProps>;
};
// @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)
```
+2
View File
@@ -26,6 +26,8 @@ import {
createExtensionDataRef,
} from '@backstage/frontend-plugin-api';
export { useTechdocsReaderIconLinkProps } from './hooks';
/** @alpha */
export type { TechDocsAddonOptions, TechDocsAddonLocations } from './types';
@@ -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: <DocsIcon />,
href: buildTechDocsURL(entity, viewTechdocLink),
};
}
/**
* Hook for use within TechDocs addons that provides access to the underlying
+50 -1
View File
@@ -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<undefined>;
},
{},
{
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<IconLinkVerticalProps, 'color'>;
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)
```
+30 -2
View File
@@ -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,
}),
});
+11 -1
View File
@@ -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',
});
+27
View File
@@ -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',
},
},
});
+3
View File
@@ -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"