diff --git a/.changeset/honest-ties-worry.md b/.changeset/honest-ties-worry.md
new file mode 100644
index 0000000000..74d8c591c1
--- /dev/null
+++ b/.changeset/honest-ties-worry.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder-react': patch
+---
+
+Scaffolding - Template card - button to show template entity detail
diff --git a/plugins/home/report.api.md b/plugins/home/report.api.md
index ef5a0d5a7a..eacf1a99a9 100644
--- a/plugins/home/report.api.md
+++ b/plugins/home/report.api.md
@@ -50,8 +50,7 @@ export const ComponentAccordion: (props: {
expanded?: boolean;
Content: () => JSX.Element;
Actions?: () => JSX.Element;
- Settings?: () => JSX./** @public */
- Element;
+ Settings?: () => JSX.Element;
ContextProvider?: (props: any) => JSX.Element;
}) => JSX_2.Element;
diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md
index b007fa7d49..fbd539f96e 100644
--- a/plugins/scaffolder-react/report-alpha.api.md
+++ b/plugins/scaffolder-react/report-alpha.api.md
@@ -347,6 +347,7 @@ export const scaffolderReactTranslationRef: TranslationRef<
readonly 'templateCategoryPicker.title': 'Categories';
readonly 'templateCard.noDescription': 'No description';
readonly 'templateCard.chooseButtonText': 'Choose';
+ readonly 'cardHeader.detailBtnTitle': 'Show template entity details';
readonly 'templateOutputs.title': 'Text Output';
}
>;
diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx
index 9ae2a507dd..3c7eef5f8d 100644
--- a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx
+++ b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx
@@ -24,12 +24,21 @@ import {
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
-import { starredEntitiesApiRef } from '@backstage/plugin-catalog-react';
+import {
+ entityRouteRef,
+ starredEntitiesApiRef,
+} from '@backstage/plugin-catalog-react';
import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog';
import Observable from 'zen-observable';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
+const mountedRoutes = {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+};
+
describe('CardHeader', () => {
it('should select the correct theme from the theme provider from the header', async () => {
// Can't really test what we want here.
@@ -64,6 +73,7 @@ describe('CardHeader', () => {
/>
,
+ mountedRoutes,
);
expect(mockTheme.getPageTheme).toHaveBeenCalledWith({ themeId: 'service' });
@@ -93,6 +103,7 @@ describe('CardHeader', () => {
}}
/>
,
+ mountedRoutes,
);
expect(getByText('service')).toBeInTheDocument();
@@ -118,6 +129,7 @@ describe('CardHeader', () => {
,
+ mountedRoutes,
);
const favorite = getByRole('button', { name: 'Add to favorites' });
@@ -129,6 +141,38 @@ describe('CardHeader', () => {
);
});
+ it('renders TemplateDetailButton with link to entity page', async () => {
+ const { getByTitle } = await renderInTestApp(
+
+
+ ,
+ mountedRoutes,
+ );
+
+ const detailButton = getByTitle('Show template entity details');
+ const link = detailButton.querySelector('a');
+ expect(link).toBeInTheDocument();
+ });
+
it('should render the name of the entity', async () => {
const { getByText } = await renderInTestApp(
{
}}
/>
,
+ mountedRoutes,
);
expect(getByText('bob')).toBeInTheDocument();
@@ -182,6 +227,7 @@ describe('CardHeader', () => {
}}
/>
,
+ mountedRoutes,
);
expect(getByText('Iamtitle')).toBeInTheDocument();
diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.tsx
index 467d26ce76..5e5c6fe8bf 100644
--- a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.tsx
+++ b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.tsx
@@ -15,10 +15,11 @@
*/
import React from 'react';
-import { Theme, makeStyles, useTheme } from '@material-ui/core/styles';
+import { makeStyles, Theme, useTheme } from '@material-ui/core/styles';
import { ItemCardHeader } from '@backstage/core-components';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { FavoriteEntity } from '@backstage/plugin-catalog-react';
+import { TemplateDetailButton } from './TemplateDetailButton.tsx';
const useStyles = makeStyles<
Theme,
@@ -66,7 +67,11 @@ export const CardHeader = (props: CardHeaderProps) => {
);
diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx
index 673f1e4a9e..e25952131b 100644
--- a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx
+++ b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx
@@ -33,6 +33,12 @@ import { permissionApiRef } from '@backstage/plugin-permission-react';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { SWRConfig } from 'swr';
+const mountedRoutes = {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+};
+
describe('TemplateCard', () => {
it('should render the card title', async () => {
const mockTemplate: TemplateEntityV1beta3 = {
@@ -59,6 +65,7 @@ describe('TemplateCard', () => {
>
,
+ mountedRoutes,
);
expect(getByText('bob')).toBeInTheDocument();
@@ -89,6 +96,7 @@ describe('TemplateCard', () => {
>
,
+ mountedRoutes,
);
const description = getByText('hello');
@@ -121,6 +129,7 @@ describe('TemplateCard', () => {
>
,
+ mountedRoutes,
);
expect(getByText('No description')).toBeInTheDocument();
@@ -151,6 +160,7 @@ describe('TemplateCard', () => {
>
,
+ mountedRoutes,
);
expect(queryByTestId('template-card-separator')).toBeInTheDocument();
@@ -187,6 +197,7 @@ describe('TemplateCard', () => {
>
,
+ mountedRoutes,
);
for (const tag of mockTemplate.metadata.tags!) {
@@ -227,11 +238,7 @@ describe('TemplateCard', () => {
>
,
- {
- mountedRoutes: {
- '/catalog/:kind/:namespace/:name': entityRouteRef,
- },
- },
+ mountedRoutes,
);
expect(queryByTestId('template-card-separator')).toBeInTheDocument();
@@ -272,11 +279,7 @@ describe('TemplateCard', () => {
>
,
- {
- mountedRoutes: {
- '/catalog/:kind/:namespace/:name': entityRouteRef,
- },
- },
+ mountedRoutes,
);
expect(queryByTestId('template-card-separator')).toBeInTheDocument();
@@ -321,11 +324,7 @@ describe('TemplateCard', () => {
>
,
- {
- mountedRoutes: {
- '/catalog/:kind/:namespace/:name': entityRouteRef,
- },
- },
+ mountedRoutes,
);
expect(queryByTestId('template-card-separator')).not.toBeInTheDocument();
@@ -364,17 +363,13 @@ describe('TemplateCard', () => {
>
,
- {
- mountedRoutes: {
- '/catalog/:kind/:namespace/:name': entityRouteRef,
- },
- },
+ mountedRoutes,
);
expect(getByRole('link', { name: /.*my-test-user$/ })).toBeInTheDocument();
expect(getByRole('link', { name: /.*my-test-user$/ })).toHaveAttribute(
'href',
- '/catalog/group/default/my-test-user',
+ '/catalog/default/group/my-test-user',
);
});
@@ -404,11 +399,7 @@ describe('TemplateCard', () => {
>
,
- {
- mountedRoutes: {
- '/catalog/:kind/:namespace/:name': entityRouteRef,
- },
- },
+ mountedRoutes,
);
expect(getByRole('button', { name: 'Choose' })).toBeInTheDocument();
@@ -448,11 +439,7 @@ describe('TemplateCard', () => {
,
- {
- mountedRoutes: {
- '/catalog/:kind/:namespace/:name': entityRouteRef,
- },
- },
+ mountedRoutes,
);
expect(queryByText('Choose')).toBeNull();
diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateDetailButton.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateDetailButton.tsx
new file mode 100644
index 0000000000..1a352d8967
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateDetailButton.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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 React from 'react';
+import Tooltip from '@material-ui/core/Tooltip';
+import IconButton from '@material-ui/core/IconButton';
+import Typography from '@material-ui/core/Typography';
+import DescriptionIcon from '@material-ui/icons/Description';
+import { Link } from '@backstage/core-components';
+import {
+ entityRouteParams,
+ entityRouteRef,
+} from '@backstage/plugin-catalog-react';
+import { useRouteRef } from '@backstage/core-plugin-api';
+import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
+import { scaffolderReactTranslationRef } from '../../../translation';
+import { useTranslationRef } from '@backstage/frontend-plugin-api';
+
+export interface TemplateDetailButtonProps {
+ template: Entity;
+}
+
+export const TemplateDetailButton = ({
+ template,
+}: TemplateDetailButtonProps) => {
+ const catalogEntityRoute = useRouteRef(entityRouteRef);
+ const { t } = useTranslationRef(scaffolderReactTranslationRef);
+ const entityRef = stringifyEntityRef(template);
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/scaffolder-react/src/translation.ts b/plugins/scaffolder-react/src/translation.ts
index f490b52064..c097d78e3e 100644
--- a/plugins/scaffolder-react/src/translation.ts
+++ b/plugins/scaffolder-react/src/translation.ts
@@ -44,6 +44,9 @@ export const scaffolderReactTranslationRef = createTranslationRef({
noDescription: 'No description',
chooseButtonText: 'Choose',
},
+ cardHeader: {
+ detailBtnTitle: 'Show template entity details',
+ },
templateOutputs: {
title: 'Text Output',
},