From 6ed42b764ceb163da691cfcfe5eecc662753737e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Vit=C3=A1sek?= Date: Tue, 18 Mar 2025 11:26:20 +0100 Subject: [PATCH] feat: add TemplateDetailButton to CardHeader and update tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ladislav Vitásek --- .changeset/honest-ties-worry.md | 5 ++ plugins/home/report.api.md | 3 +- plugins/scaffolder-react/report-alpha.api.md | 1 + .../TemplateCard/CardHeader.test.tsx | 48 ++++++++++++++- .../components/TemplateCard/CardHeader.tsx | 9 ++- .../TemplateCard/TemplateCard.test.tsx | 49 ++++++--------- .../TemplateCard/TemplateDetailButton.tsx | 61 +++++++++++++++++++ plugins/scaffolder-react/src/translation.ts | 3 + 8 files changed, 143 insertions(+), 36 deletions(-) create mode 100644 .changeset/honest-ties-worry.md create mode 100644 plugins/scaffolder-react/src/next/components/TemplateCard/TemplateDetailButton.tsx 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) => {
{type}
- + +
); 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', },