From 08f177b91084d42bb97aad679271f20dc80a8b46 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Fri, 19 May 2023 12:58:25 -0500 Subject: [PATCH 1/4] feat(catalog): Link launch template in Entity's AboutCard Signed-off-by: Carlos Esteban Lopez --- .changeset/chatty-seals-tap.md | 6 +++++ packages/app/src/App.tsx | 1 + plugins/catalog/package.json | 1 + .../src/components/AboutCard/AboutCard.tsx | 24 +++++++++++++++++-- plugins/catalog/src/plugin.ts | 7 +++++- plugins/catalog/src/routes.ts | 6 +++++ yarn.lock | 1 + 7 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 .changeset/chatty-seals-tap.md diff --git a/.changeset/chatty-seals-tap.md b/.changeset/chatty-seals-tap.md new file mode 100644 index 0000000000..384a89e6ce --- /dev/null +++ b/.changeset/chatty-seals-tap.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +'example-app': patch +--- + +Add link from Template entity to the scaffolder launch page for the template in the AboutCard. diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index bc9eac8c64..0efcafba26 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -142,6 +142,7 @@ const app = createApp({ bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, viewTechDoc: techdocsPlugin.routes.docRoot, + selectedTemplateRoute: scaffolderPlugin.routes.selectedTemplate, }); bind(apiDocsPlugin.externalRoutes, { registerApi: catalogImportPlugin.routes.importPage, diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 21983f762e..16176b9662 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -40,6 +40,7 @@ "@backstage/integration-react": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-search-react": "workspace:^", "@backstage/theme": "workspace:^", diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index cb48be2b26..4be201b555 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -49,12 +49,14 @@ import { IconButton, makeStyles, } from '@material-ui/core'; +import AddIcon from '@material-ui/icons/Add'; import CachedIcon from '@material-ui/icons/Cached'; import DocsIcon from '@material-ui/icons/Description'; import EditIcon from '@material-ui/icons/Edit'; import React, { useCallback } from 'react'; -import { viewTechDocRouteRef } from '../../routes'; +import { selectedTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { AboutContent } from './AboutContent'; +import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; const useStyles = makeStyles({ gridItemCard: { @@ -97,6 +99,7 @@ export function AboutCard(props: AboutCardProps) { const alertApi = useApi(alertApiRef); const errorApi = useApi(errorApiRef); const viewTechdocLink = useRouteRef(viewTechDocRouteRef); + const templateRoute = useRouteRef(selectedTemplateRouteRef); const entitySourceLocation = getEntitySourceLocation( entity, @@ -126,6 +129,23 @@ export function AboutCard(props: AboutCardProps) { }), }; + const subHeaderLinks = [viewInSource, viewInTechDocs]; + + if (isTemplateEntityV1beta3(entity)) { + const launchTemplate: IconLinkVerticalProps = { + label: 'Launch Template', + icon: , + href: + templateRoute && + templateRoute({ + templateName: entity.metadata.name, + namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, + }), + }; + + subHeaderLinks.push(launchTemplate); + } + let cardClass = ''; if (variant === 'gridItem') { cardClass = classes.gridItemCard; @@ -179,7 +199,7 @@ export function AboutCard(props: AboutCardProps) { } - subheader={} + subheader={} /> diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index ea6ddcaff2..22c85a755d 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -21,7 +21,11 @@ import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { createComponentRouteRef, viewTechDocRouteRef } from './routes'; +import { + createComponentRouteRef, + selectedTemplateRouteRef, + viewTechDocRouteRef, +} from './routes'; import { createApiFactory, createComponentExtension, @@ -77,6 +81,7 @@ export const catalogPlugin = createPlugin({ externalRoutes: { createComponent: createComponentRouteRef, viewTechDoc: viewTechDocRouteRef, + selectedTemplateRoute: selectedTemplateRouteRef, }, __experimentalConfigure( options?: CatalogInputPluginOptions, diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 5c0d195d74..2d9667d8e2 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -30,6 +30,12 @@ export const viewTechDocRouteRef = createExternalRouteRef({ params: ['namespace', 'kind', 'name'], }); +export const selectedTemplateRouteRef = createExternalRouteRef({ + id: 'selected-template', + optional: true, + params: ['namespace', 'templateName'], +}); + export const rootRouteRef = createRouteRef({ id: 'catalog', }); diff --git a/yarn.lock b/yarn.lock index 2e6f814cf5..6ce19fdede 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5739,6 +5739,7 @@ __metadata: "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" + "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" "@backstage/test-utils": "workspace:^" From 76cd142840953026c2f355ac4b16c8c3a5f4e882 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Fri, 19 May 2023 15:02:27 -0500 Subject: [PATCH 2/4] tests(catalog): Fix tests & Icon from system icons Signed-off-by: Carlos Esteban Lopez --- .changeset/chatty-seals-tap.md | 1 - plugins/catalog/api-report.md | 7 + .../components/AboutCard/AboutCard.test.tsx | 163 +++++++++++++++++- .../src/components/AboutCard/AboutCard.tsx | 10 +- .../api-report.md | 6 +- 5 files changed, 179 insertions(+), 8 deletions(-) diff --git a/.changeset/chatty-seals-tap.md b/.changeset/chatty-seals-tap.md index 384a89e6ce..5511fe4520 100644 --- a/.changeset/chatty-seals-tap.md +++ b/.changeset/chatty-seals-tap.md @@ -1,6 +1,5 @@ --- '@backstage/plugin-catalog': patch -'example-app': patch --- Add link from Template entity to the scaffolder launch page for the template in the AboutCard. diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 970d8d3251..915e89d0c6 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -105,6 +105,13 @@ export const catalogPlugin: BackstagePlugin< }, true >; + selectedTemplateRoute: ExternalRouteRef< + { + namespace: string; + templateName: string; + }, + true + >; }, CatalogInputPluginOptions >; diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index 869d6f1757..43af858300 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -30,7 +30,7 @@ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/react'; import React from 'react'; -import { viewTechDocRouteRef } from '../../routes'; +import { selectedTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { AboutCard } from './AboutCard'; describe('', () => { @@ -505,4 +505,165 @@ describe('', () => { expect(screen.getByText('View TechDocs')).toBeVisible(); expect(screen.getByText('View TechDocs').closest('a')).toBeNull(); }); + + it('renders launch template link', async () => { + const entity = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'create-react-app-template', + namespace: 'default', + }, + }; + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/create/templates/:namespace/:templateName': + selectedTemplateRouteRef, + }, + }, + ); + + expect(screen.getByText('Launch Template')).toBeVisible(); + expect(screen.getByText('Launch Template').closest('a')).toHaveAttribute( + 'href', + '/create/templates/default/create-react-app-template', + ); + }); + + it.each([ + { + testName: 'entity is not a template', + entity: { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Component', + metadata: { + name: 'create-react-app-template', + namespace: 'default', + }, + }, + }, + { + testName: 'apiVersion is not scaffolder.backstage.io/v1beta3', + entity: { + apiVersion: 'v1', + kind: 'Template', + metadata: { + name: 'create-react-app-template', + namespace: 'default', + }, + }, + }, + ])( + 'should not render launch template link when $testName', + async ({ entity }) => { + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/create/templates/:namespace/:templateName': + selectedTemplateRouteRef, + }, + }, + ); + + expect(screen.queryByText('Launch Template')).toBeNull(); + }, + ); + + it('renders disabled launch template link when route is not bound', async () => { + const entity = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'create-react-app-template', + namespace: 'default', + }, + }; + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(screen.getByText('Launch Template')).toBeVisible(); + expect(screen.getByText('Launch Template').closest('a')).toBeNull(); + }); }); diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 4be201b555..2933aea9e8 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -30,6 +30,7 @@ import { alertApiRef, errorApiRef, useApi, + useApp, useRouteRef, } from '@backstage/core-plugin-api'; import { @@ -90,8 +91,8 @@ export interface AboutCardProps { /** * Exported publicly via the EntityAboutCard */ -export function AboutCard(props: AboutCardProps) { - const { variant } = props; +export function AboutCard({ variant }: AboutCardProps) { + const app = useApp(); const classes = useStyles(); const { entity } = useEntity(); const scmIntegrationsApi = useApi(scmIntegrationsApiRef); @@ -132,9 +133,12 @@ export function AboutCard(props: AboutCardProps) { const subHeaderLinks = [viewInSource, viewInTechDocs]; if (isTemplateEntityV1beta3(entity)) { + const Icon = app.getSystemIcon('scaffolder') ?? AddIcon; + const launchTemplate: IconLinkVerticalProps = { label: 'Launch Template', - icon: , + icon: , + disabled: !templateRoute, href: templateRoute && templateRoute({ diff --git a/plugins/scaffolder-backend-module-gitlab/api-report.md b/plugins/scaffolder-backend-module-gitlab/api-report.md index f7cf6024ec..eccada5d51 100644 --- a/plugins/scaffolder-backend-module-gitlab/api-report.md +++ b/plugins/scaffolder-backend-module-gitlab/api-report.md @@ -26,8 +26,8 @@ export const createGitlabProjectAccessTokenAction: (options: { integrations: ScmIntegrationRegistry; }) => TemplateAction< { - projectId: string | number; repoUrl: string; + projectId: string | number; token?: string | undefined; name?: string | undefined; accessLevel?: number | undefined; @@ -44,8 +44,8 @@ export const createGitlabProjectDeployTokenAction: (options: { }) => TemplateAction< { name: string; - projectId: string | number; repoUrl: string; + projectId: string | number; token?: string | undefined; username?: string | undefined; scopes?: string[] | undefined; @@ -63,8 +63,8 @@ export const createGitlabProjectVariableAction: (options: { { key: string; value: string; - projectId: string | number; repoUrl: string; + projectId: string | number; variableType: string; token?: string | undefined; variableProtected?: boolean | undefined; From 62f4c6d195f5888db1c8426362fbb069758005b9 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Fri, 19 May 2023 16:20:25 -0500 Subject: [PATCH 3/4] feat(catalog): Sync external link with create-app default-app App.tsx Signed-off-by: Carlos Esteban Lopez --- .../create-app/templates/default-app/packages/app/src/App.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 056402f2a8..cac3e7f0a9 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -40,12 +40,14 @@ const app = createApp({ bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, viewTechDoc: techdocsPlugin.routes.docRoot, + selectedTemplateRoute: scaffolderPlugin.routes.selectedTemplate, }); bind(apiDocsPlugin.externalRoutes, { registerApi: catalogImportPlugin.routes.importPage, }); bind(scaffolderPlugin.externalRoutes, { registerComponent: catalogImportPlugin.routes.importPage, + viewTechDoc: techdocsPlugin.routes.docRoot, }); bind(orgPlugin.externalRoutes, { catalogIndex: catalogPlugin.routes.catalogIndex, From 62d2a70660c31fce96bcb993ee860fd562c262b7 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Tue, 23 May 2023 11:10:11 -0500 Subject: [PATCH 4/4] fix(catalog): Address PR feedback Signed-off-by: Carlos Esteban Lopez --- .changeset/chatty-seals-tap.md | 1 + packages/app/src/App.tsx | 2 +- .../default-app/packages/app/src/App.tsx | 2 +- plugins/catalog/api-report.md | 2 +- .../src/components/AboutCard/AboutCard.test.tsx | 6 +++--- .../src/components/AboutCard/AboutCard.tsx | 15 ++++++++------- plugins/catalog/src/plugin.ts | 4 ++-- plugins/catalog/src/routes.ts | 4 ++-- 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.changeset/chatty-seals-tap.md b/.changeset/chatty-seals-tap.md index 5511fe4520..01d69e1562 100644 --- a/.changeset/chatty-seals-tap.md +++ b/.changeset/chatty-seals-tap.md @@ -1,4 +1,5 @@ --- +'@backstage/create-app': patch '@backstage/plugin-catalog': patch --- diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 0efcafba26..d46774a757 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -142,7 +142,7 @@ const app = createApp({ bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, viewTechDoc: techdocsPlugin.routes.docRoot, - selectedTemplateRoute: scaffolderPlugin.routes.selectedTemplate, + createFromTemplate: scaffolderPlugin.routes.selectedTemplate, }); bind(apiDocsPlugin.externalRoutes, { registerApi: catalogImportPlugin.routes.importPage, diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index cac3e7f0a9..8d62f29c52 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -40,7 +40,7 @@ const app = createApp({ bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, viewTechDoc: techdocsPlugin.routes.docRoot, - selectedTemplateRoute: scaffolderPlugin.routes.selectedTemplate, + createFromTemplate: scaffolderPlugin.routes.selectedTemplate, }); bind(apiDocsPlugin.externalRoutes, { registerApi: catalogImportPlugin.routes.importPage, diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 915e89d0c6..6bb4fcc4a1 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -105,7 +105,7 @@ export const catalogPlugin: BackstagePlugin< }, true >; - selectedTemplateRoute: ExternalRouteRef< + createFromTemplate: ExternalRouteRef< { namespace: string; templateName: string; diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index 43af858300..e8d2657e9f 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -30,7 +30,7 @@ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/react'; import React from 'react'; -import { selectedTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; +import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { AboutCard } from './AboutCard'; describe('', () => { @@ -545,7 +545,7 @@ describe('', () => { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, '/create/templates/:namespace/:templateName': - selectedTemplateRouteRef, + createFromTemplateRouteRef, }, }, ); @@ -612,7 +612,7 @@ describe('', () => { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, '/create/templates/:namespace/:templateName': - selectedTemplateRouteRef, + createFromTemplateRouteRef, }, }, ); diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 2933aea9e8..a08a9be5a4 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { ANNOTATION_EDIT_URL, ANNOTATION_LOCATION, @@ -42,6 +41,7 @@ import { getEntitySourceLocation, useEntity, } from '@backstage/plugin-catalog-react'; +import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { Card, CardContent, @@ -50,14 +50,14 @@ import { IconButton, makeStyles, } from '@material-ui/core'; -import AddIcon from '@material-ui/icons/Add'; +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import CachedIcon from '@material-ui/icons/Cached'; import DocsIcon from '@material-ui/icons/Description'; import EditIcon from '@material-ui/icons/Edit'; import React, { useCallback } from 'react'; -import { selectedTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; + +import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { AboutContent } from './AboutContent'; -import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; const useStyles = makeStyles({ gridItemCard: { @@ -91,7 +91,8 @@ export interface AboutCardProps { /** * Exported publicly via the EntityAboutCard */ -export function AboutCard({ variant }: AboutCardProps) { +export function AboutCard(props: AboutCardProps) { + const { variant } = props; const app = useApp(); const classes = useStyles(); const { entity } = useEntity(); @@ -100,7 +101,7 @@ export function AboutCard({ variant }: AboutCardProps) { const alertApi = useApi(alertApiRef); const errorApi = useApi(errorApiRef); const viewTechdocLink = useRouteRef(viewTechDocRouteRef); - const templateRoute = useRouteRef(selectedTemplateRouteRef); + const templateRoute = useRouteRef(createFromTemplateRouteRef); const entitySourceLocation = getEntitySourceLocation( entity, @@ -133,7 +134,7 @@ export function AboutCard({ variant }: AboutCardProps) { const subHeaderLinks = [viewInSource, viewInTechDocs]; if (isTemplateEntityV1beta3(entity)) { - const Icon = app.getSystemIcon('scaffolder') ?? AddIcon; + const Icon = app.getSystemIcon('scaffolder') ?? CreateComponentIcon; const launchTemplate: IconLinkVerticalProps = { label: 'Launch Template', diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 22c85a755d..711f52767a 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -23,7 +23,7 @@ import { } from '@backstage/plugin-catalog-react'; import { createComponentRouteRef, - selectedTemplateRouteRef, + createFromTemplateRouteRef, viewTechDocRouteRef, } from './routes'; import { @@ -81,7 +81,7 @@ export const catalogPlugin = createPlugin({ externalRoutes: { createComponent: createComponentRouteRef, viewTechDoc: viewTechDocRouteRef, - selectedTemplateRoute: selectedTemplateRouteRef, + createFromTemplate: createFromTemplateRouteRef, }, __experimentalConfigure( options?: CatalogInputPluginOptions, diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 2d9667d8e2..ab70a3c551 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -30,8 +30,8 @@ export const viewTechDocRouteRef = createExternalRouteRef({ params: ['namespace', 'kind', 'name'], }); -export const selectedTemplateRouteRef = createExternalRouteRef({ - id: 'selected-template', +export const createFromTemplateRouteRef = createExternalRouteRef({ + id: 'create-from-template', optional: true, params: ['namespace', 'templateName'], });