From 826e279e6204b5cc6d9505eac98983694ff524e3 Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Wed, 12 Jan 2022 16:45:14 +0200 Subject: [PATCH 01/12] feat(catalog-import): make catalog filename configurable Signed-off-by: Mikko Korhonen --- app-config.yaml | 2 ++ packages/create-app/templates/default-app/app-config.yaml.hbs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app-config.yaml b/app-config.yaml index b43a229ae9..38efb3bb87 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -194,6 +194,8 @@ integrations: secretAccessKey: ${AWS_SECRET_ACCESS_KEY} catalog: + import: + entityFilename: catalog-info.yaml rules: - allow: - Component diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index c804b4b561..380c7c5152 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -80,6 +80,8 @@ scaffolder: # see https://backstage.io/docs/features/software-templates/configuration for software template options catalog: + import: + entityFilename: catalog-info.yaml rules: - allow: [Component, System, API, Group, User, Resource, Location] locations: From b20709491de27d618b99d380bdcf1008bc28fea6 Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Thu, 13 Jan 2022 11:00:35 +0200 Subject: [PATCH 02/12] feat(catalog-import): expose catalog filename to frontend Signed-off-by: Mikko Korhonen --- plugins/catalog-import/config.d.ts | 33 +++++++++++++++++++++++++++++ plugins/catalog-import/package.json | 6 ++++-- 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 plugins/catalog-import/config.d.ts diff --git a/plugins/catalog-import/config.d.ts b/plugins/catalog-import/config.d.ts new file mode 100644 index 0000000000..b5c5edb89a --- /dev/null +++ b/plugins/catalog-import/config.d.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 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. + */ + +export interface Config { + /** + * Configuration options for the catalog plugin. + */ + catalog?: { + /** + * List of import flow specific options and attributes + */ + import?: { + /** + * Catalog entity descriptor filename, defaults to "catalog-info.yaml" + * @visibility frontend + */ + entityFilename?: string; + }; + }; +} diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index ff9fcb24c2..5097f5cc9b 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -69,6 +69,8 @@ "msw": "^0.35.0" }, "files": [ - "dist" - ] + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" } From 581bed351228bd347c7326ea774527794a617893 Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Wed, 12 Jan 2022 17:04:00 +0200 Subject: [PATCH 03/12] feat(catalog-import): read catalog filename from app config Signed-off-by: Mikko Korhonen --- .../src/api/CatalogImportClient.ts | 23 ++++++++++++++----- .../ImportInfoCard/ImportInfoCard.tsx | 8 +++++-- .../PreviewCatalogInfoComponent.tsx | 8 ++++++- .../StepPrepareCreatePullRequest.tsx | 9 ++++++-- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index bc59657cad..1b84fba12f 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -87,13 +87,17 @@ export class CatalogImportClient implements CatalogImportApi { const ghConfig = getGithubIntegrationConfig(this.scmIntegrationsApi, url); if (!ghConfig) { const other = this.scmIntegrationsApi.byUrl(url); + const catalogFilename = + this.configApi.getOptionalString('catalog.import.entityFilename') ?? + 'catalog-info.yaml'; + if (other) { throw new Error( - `The ${other.title} integration only supports full URLs to catalog-info.yaml files. Did you try to pass in the URL of a directory instead?`, + `The ${other.title} integration only supports full URLs to ${catalogFilename} files. Did you try to pass in the URL of a directory instead?`, ); } throw new Error( - 'This URL was not recognized as a valid GitHub URL because there was no configured integration that matched the given host name. You could try to paste the full URL to a catalog-info.yaml file instead.', + `This URL was not recognized as a valid GitHub URL because there was no configured integration that matched the given host name. You could try to paste the full URL to a ${catalogFilename} file instead.`, ); } @@ -127,9 +131,12 @@ export class CatalogImportClient implements CatalogImportApi { const appTitle = this.configApi.getOptionalString('app.title') ?? 'Backstage'; const appBaseUrl = this.configApi.getString('app.baseUrl'); + const catalogFilename = + this.configApi.getOptionalString('catalog.import.entityFilename') ?? + 'catalog-info.yaml'; return { - title: 'Add catalog-info.yaml config file', + title: `Add ${catalogFilename} config file`, body: `This pull request adds a **Backstage entity metadata file** \ to this repository so that the component can be added to the \ [${appTitle} software catalog](${appBaseUrl}).\n\nAfter this pull request is merged, \ @@ -221,8 +228,10 @@ the component will become available.\n\nFor more information, read an \ auth: token, baseUrl: githubIntegrationConfig.apiBaseUrl, }); - const catalogFileName = 'catalog-info.yaml'; - const query = `repo:${owner}/${repo}+filename:${catalogFileName}`; + const catalogFilename = + this.configApi.getOptionalString('catalog.import.entityFilename') ?? + 'catalog-info.yaml'; + const query = `repo:${owner}/${repo}+filename:${catalogFilename}`; const searchResult = await octo.search.code({ q: query }).catch(e => { throw new Error( @@ -295,7 +304,9 @@ the component will become available.\n\nFor more information, read an \ }); const branchName = 'backstage-integration'; - const fileName = 'catalog-info.yaml'; + const fileName = + this.configApi.getOptionalString('catalog.import.entityFilename') ?? + 'catalog-info.yaml'; const repoData = await octo.repos .get({ diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx index 2fab223f20..ef72f79c8a 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx +++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx @@ -28,6 +28,10 @@ export const ImportInfoCard = () => { const integrations = configApi.getConfig('integrations'); const hasGithubIntegration = integrations.has('github'); + const catalogFilename = + configApi.getOptionalString('catalog.import.entityFilename') ?? + 'catalog-info.yaml'; + return ( { Example: https://github.com/backstage/backstage - The wizard discovers all catalog-info.yaml files in the + The wizard discovers all {catalogFilename} files in the repository, previews the entities, and adds them to the {appTitle}{' '} catalog. {catalogImportApi.preparePullRequest && ( If no entities are found, the wizard will prepare a Pull Request - that adds an example catalog-info.yaml and prepares + that adds an example {catalogFilename} and prepares the {appTitle} catalog to load all entities as soon as the Pull Request is merged. diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx index d0f29c00e2..d97f3a58f6 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx @@ -19,6 +19,7 @@ import { Card, CardContent, CardHeader } from '@material-ui/core'; import React from 'react'; import YAML from 'yaml'; import { CodeSnippet } from '@backstage/core-components'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { trimEnd } from 'lodash'; type Props = { @@ -32,11 +33,16 @@ export const PreviewCatalogInfoComponent = ({ entities, classes, }: Props) => { + const configApi = useApi(configApiRef); + const catalogFilename = + configApi.getOptionalString('catalog.import.entityFilename') ?? + 'catalog-info.yaml'; + return ( {`${trimEnd(repositoryUrl, '/')}/catalog-info.yaml`} + {`${trimEnd(repositoryUrl, '/')}/${catalogFilename}`} } /> diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx index 077d99c4ba..d511294b61 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { configApiRef, errorApiRef, useApi } from '@backstage/core-plugin-api'; import { assertError } from '@backstage/errors'; import { catalogApiRef, @@ -102,10 +102,15 @@ export const StepPrepareCreatePullRequest = ({ const catalogApi = useApi(catalogApiRef); const catalogImportApi = useApi(catalogImportApiRef); const errorApi = useApi(errorApiRef); + const configApi = useApi(configApiRef); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(); + const catalogFilename = + configApi.getOptionalString('catalog.import.entityFilename') ?? + 'catalog-info.yaml'; + const { loading: prDefaultsLoading, value: prDefaults, @@ -193,7 +198,7 @@ export const StepPrepareCreatePullRequest = ({ <> You entered a link to a {analyzeResult.integrationType} repository but a{' '} - catalog-info.yaml could not be found. Use this form to open + {catalogFilename} could not be found. Use this form to open a Pull Request that creates one. From 1bd29cd9aecf9cef13476a69c17c5decc55b368d Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Thu, 13 Jan 2022 11:07:43 +0200 Subject: [PATCH 04/12] feat(catalog-import): make catalog import example paths customizable Signed-off-by: Mikko Korhonen --- .../ImportInfoCard/ImportInfoCard.tsx | 17 +++++++++++------ .../StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx | 4 +++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx index ef72f79c8a..4d78542e60 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx +++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx @@ -20,7 +20,15 @@ import { Chip, Typography } from '@material-ui/core'; import React from 'react'; import { catalogImportApiRef } from '../../api'; -export const ImportInfoCard = () => { +type Props = { + exampleLocationUrl?: string; + exampleRepositoryUrl?: string; +}; + +export const ImportInfoCard = ({ + exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + exampleRepositoryUrl = 'https://github.com/backstage/backstage', +}: Props) => { const configApi = useApi(configApiRef); const appTitle = configApi.getOptional('app.title') || 'Backstage'; const catalogImportApi = useApi(catalogImportApiRef); @@ -45,10 +53,7 @@ export const ImportInfoCard = () => { Link to an existing entity file - Example:{' '} - - https://github.com/backstage/backstage/blob/master/catalog-info.yaml - + Example: {exampleLocationUrl} The wizard analyzes the file, previews the entities, and adds them to @@ -61,7 +66,7 @@ export const ImportInfoCard = () => { - Example: https://github.com/backstage/backstage + Example: {exampleRepositoryUrl} The wizard discovers all {catalogFilename} files in the diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx index 63f3c10fe3..83377d6b2f 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx @@ -36,6 +36,7 @@ type Props = { ) => void; disablePullRequest?: boolean; analysisUrl?: string; + exampleLocationUrl?: string; }; /** @@ -49,6 +50,7 @@ export const StepInitAnalyzeUrl = ({ onAnalysis, analysisUrl = '', disablePullRequest = false, + exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', }: Props) => { const errorApi = useApi(errorApiRef); const catalogImportApi = useApi(catalogImportApiRef); @@ -138,7 +140,7 @@ export const StepInitAnalyzeUrl = ({ fullWidth id="url" label="Repository URL" - placeholder="https://github.com/backstage/backstage/blob/master/catalog-info.yaml" + placeholder={exampleLocationUrl} helperText="Enter the full path to your entity file to start tracking your component" margin="normal" variant="outlined" From 307f17372e667f111a2fc1371102a9b407d1ba4b Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Thu, 13 Jan 2022 12:30:39 +0200 Subject: [PATCH 05/12] feat(catalog-import): make catalog import branch name customizable Signed-off-by: Mikko Korhonen --- app-config.yaml | 1 + .../create-app/templates/default-app/app-config.yaml.hbs | 1 + plugins/catalog-import/config.d.ts | 7 +++++++ plugins/catalog-import/src/api/CatalogImportClient.ts | 4 +++- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app-config.yaml b/app-config.yaml index 38efb3bb87..b84b5de2f2 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -196,6 +196,7 @@ integrations: catalog: import: entityFilename: catalog-info.yaml + pullRequestBranchName: backstage-integration rules: - allow: - Component diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 380c7c5152..46e2ecbb05 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -82,6 +82,7 @@ scaffolder: catalog: import: entityFilename: catalog-info.yaml + pullRequestBranchName: backstage-integration rules: - allow: [Component, System, API, Group, User, Resource, Location] locations: diff --git a/plugins/catalog-import/config.d.ts b/plugins/catalog-import/config.d.ts index b5c5edb89a..0cc49e418b 100644 --- a/plugins/catalog-import/config.d.ts +++ b/plugins/catalog-import/config.d.ts @@ -28,6 +28,13 @@ export interface Config { * @visibility frontend */ entityFilename?: string; + /** + * A branch name used in pull request when registering existing component via UI + * Valid git refname required, see: https://git-scm.com/docs/git-check-ref-format + * Defaults to "backstage-integration" + * @visibility frontend + */ + pullRequestBranchName?: string; }; }; } diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 1b84fba12f..218814d2fb 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -303,7 +303,9 @@ the component will become available.\n\nFor more information, read an \ baseUrl: githubIntegrationConfig.apiBaseUrl, }); - const branchName = 'backstage-integration'; + const branchName = + this.configApi.getOptionalString('catalog.pullRequestBranchName') ?? + 'backstage-integration'; const fileName = this.configApi.getOptionalString('catalog.import.entityFilename') ?? 'catalog-info.yaml'; From 4d17cc54248c3ba00caba3b3969a77969414b815 Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Fri, 14 Jan 2022 09:35:16 +0200 Subject: [PATCH 06/12] test(catalog-import): update and tests for custom catalog filename and branch name Signed-off-by: Mikko Korhonen --- .../src/api/CatalogImportClient.test.ts | 168 ++++++++++++++++++ .../PreviewCatalogInfoComponent.test.tsx | 69 +++++-- .../StepPrepareCreatePullRequest.test.tsx | 7 +- 3 files changed, 228 insertions(+), 16 deletions(-) diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 202a7aba85..fa63825af1 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -396,6 +396,84 @@ describe('CatalogImportClient', () => { ], }); }); + + it('should find location with custom catalog filename', async () => { + const repositoryUrl = 'https://github.com/acme-corp/our-awesome-api'; + const entityFilename = 'anvil.yaml'; + + catalogImportClient = new CatalogImportClient({ + discoveryApi, + scmAuthApi, + scmIntegrationsApi, + identityApi, + catalogApi, + configApi: new ConfigReader({ + catalog: { + import: { + entityFilename, + }, + }, + }), + }); + + (new Octokit().search.code as any as jest.Mock).mockImplementationOnce( + async params => ({ + data: { + total_count: 1, + items: [{ path: params.q.split('+filename:').slice(-1)[0] }], + }, + }), + ); + + catalogApi.addLocation.mockImplementation(async ({ type, target }) => ({ + location: { + id: 'id-0', + type: type ?? 'url', + target, + }, + entities: [ + { + apiVersion: '1', + kind: 'Location', + metadata: { + name: 'my-entity', + namespace: 'my-namespace', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'my-entity', + namespace: 'my-namespace', + }, + }, + ], + })); + + await expect( + catalogImportClient.analyzeUrl(repositoryUrl), + ).resolves.toEqual({ + locations: [ + { + entities: [ + { + kind: 'Location', + namespace: 'my-namespace', + name: 'my-entity', + }, + { + kind: 'Component', + namespace: 'my-namespace', + name: 'my-entity', + }, + ], + target: `${repositoryUrl}/blob/main/${entityFilename}`, + }, + ], + type: 'locations', + }); + }); }); describe('submitPullRequest', () => { @@ -443,6 +521,67 @@ describe('CatalogImportClient', () => { base: 'main', }); }); + + it('should create GitHub pull request with custom filename and branch name', async () => { + const entityFilename = 'anvil.yaml'; + const pullRequestBranchName = 'anvil-integration'; + + catalogImportClient = new CatalogImportClient({ + discoveryApi, + scmAuthApi, + scmIntegrationsApi, + identityApi, + catalogApi, + configApi: new ConfigReader({ + catalog: { + import: { + entityFilename, + pullRequestBranchName, + }, + }, + }), + }); + + await expect( + catalogImportClient.submitPullRequest({ + repositoryUrl: 'https://github.com/acme-corp/our-awesome-api', + fileContent: '', + title: `Add ${entityFilename} config file`, + body: `Add ${entityFilename} config file`, + }), + ).resolves.toEqual( + expect.objectContaining({ + link: 'http://pull/request/0', + location: `https://github.com/acme-corp/our-awesome-api/blob/main/${entityFilename}`, + }), + ); + + expect( + (new Octokit().git.createRef as any as jest.Mock).mock.calls[0][0], + ).toEqual( + expect.objectContaining({ + ref: `refs/heads/${pullRequestBranchName}`, + }), + ); + + expect( + (new Octokit().repos.createOrUpdateFileContents as any as jest.Mock) + .mock.calls[0][0], + ).toEqual( + expect.objectContaining({ + path: entityFilename, + branch: pullRequestBranchName, + }), + ); + + expect( + (new Octokit().pulls.create as any as jest.Mock).mock.calls[0][0], + ).toEqual( + expect.objectContaining({ + head: pullRequestBranchName, + }), + ); + }); }); describe('preparePullRequest', () => { @@ -452,5 +591,34 @@ describe('CatalogImportClient', () => { body: expect.any(String), }); }); + + test('should prepare pull request details with custom filename', async () => { + const entityFilename = 'anvil.yaml'; + const pullRequestBranchName = 'anvil-integration'; + + catalogImportClient = new CatalogImportClient({ + discoveryApi, + scmAuthApi, + scmIntegrationsApi, + identityApi, + catalogApi, + configApi: new ConfigReader({ + catalog: { + import: { + entityFilename, + pullRequestBranchName, + }, + }, + app: { + baseUrl: 'https://demo.backstage.io/', + }, + }), + }); + + await expect(catalogImportClient.preparePullRequest()).resolves.toEqual({ + title: `Add ${entityFilename} config file`, + body: expect.any(String), + }); + }); }); }); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx index fe5e76c968..aac54e084a 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx @@ -15,6 +15,9 @@ */ import { Entity } from '@backstage/catalog-model'; +import { ApiProvider } from '@backstage/core-app-api'; +import { configApiRef } from '@backstage/core-plugin-api'; +import { TestApiRegistry, MockConfigApi } from '@backstage/test-utils'; import { makeStyles } from '@material-ui/core'; import { render, screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; @@ -44,13 +47,19 @@ const entities: Entity[] = [ }, ]; +const mockConfigApi = new MockConfigApi({}); + +const apis = TestApiRegistry.from([configApiRef, mockConfigApi]); + describe('', () => { it('renders without exploding', () => { render( - , + + + , ); const repositoryUrl = screen.getByText( @@ -67,11 +76,13 @@ describe('', () => { const { result } = renderHook(() => useStyles()); render( - , + + + , ); const repositoryUrl = screen.getByText( @@ -88,11 +99,13 @@ describe('', () => { const { result } = renderHook(() => useStyles()); render( - , + + + , ); const repositoryUrl = screen.getByText( @@ -104,4 +117,32 @@ describe('', () => { expect(kindText).toBeInTheDocument(); expect(kindText).not.toBeVisible(); }); + + it('renders with custom catalog filename', () => { + render( + + + , + ); + + const repositoryUrl = screen.getByText( + 'http://acme-corp/awesome-api/anvil.yaml', + ); + expect(repositoryUrl).toBeInTheDocument(); + expect(repositoryUrl).toBeVisible(); + }); }); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index 3b6f353155..8d26657bb3 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ -import { errorApiRef } from '@backstage/core-plugin-api'; +import { configApiRef, errorApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider, MockConfigApi } from '@backstage/test-utils'; import { TextField } from '@material-ui/core'; import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -53,12 +53,15 @@ describe('', () => { post: jest.fn(), }; + const configApi = new MockConfigApi({}); + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( {children} From b40159ff3a2b73782ca240eb1e1039bca966b757 Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Fri, 14 Jan 2022 09:50:22 +0200 Subject: [PATCH 07/12] chore(catalog-import): update README with relevant information how to customzie layout Obsolete instructions were used, updated with up-to-date information from changelog https://github.com/backstage/backstage/blob/master/plugins/catalog-import/CHANGELOG.md#minor-changes-1 Signed-off-by: Mikko Korhonen --- plugins/catalog-import/README.md | 61 ++++++++++++++++---------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md index bad48a8486..f492619690 100644 --- a/plugins/catalog-import/README.md +++ b/plugins/catalog-import/README.md @@ -35,41 +35,42 @@ import { CatalogImportPage } from '@backstage/plugin-catalog-import'; ## Customizations -### Disable the creation of Pull Requests +A custom layout can be passed to the import page, as it's already +supported by the search page. If no custom layout is passed, the default layout +is used. -The pull request feature can be disabled by options that are passed to the `CatalogImportPage`: +```typescript +}> + +
+ + + + Start tracking your component in Backstage by adding it to the + software catalog. + + -```tsx -// packages/app/src/App.tsx + + + Hello World + -} -/> + + + + + + + ``` -### Customize the title and body of the Pull Request - -The pull request form is filled with a default title and body. -This can be configured by options that are passed to the `CatalogImportPage`: - -```tsx -// packages/app/src/App.tsx - - ({ - title: 'My title', - body: 'My **markdown** body', - }), - }} - /> - } -/> -``` +Previously it was possible to disable and customize the automatic pull request +feature by passing options to `` (`pullRequest.disable` and +`pullRequest.preparePullRequest`). This functionality is moved to the +`CatalogImportApi` which now provides an optional `preparePullRequest()` +function. The function can either be overridden to generate a different content +for the pull request, or removed to disable this feature. ## Development From 2e8764b95fa2095b7b99eed7e5613fc819c3e73c Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Fri, 14 Jan 2022 11:43:11 +0200 Subject: [PATCH 08/12] chore(catalog-import): add change set for import flow customizations Signed-off-by: Mikko Korhonen --- .changeset/tame-ads-exercise.md | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .changeset/tame-ads-exercise.md diff --git a/.changeset/tame-ads-exercise.md b/.changeset/tame-ads-exercise.md new file mode 100644 index 0000000000..a2e11393a6 --- /dev/null +++ b/.changeset/tame-ads-exercise.md @@ -0,0 +1,34 @@ +--- +'@backstage/plugin-catalog-import': minor +--- + +Make filename, branch name and examples URLs used in catalog import customizable. + +Catalog backend ingestion loop can be already configured to fetch targets with custom catalog filename (other than `catalog-info.yaml`). It's now possible to customize said filename and branch name used in pull requests created by catalog import flow too. This allows organizations to further customize Backstage experience and to better reflect their branding. + +Filename (default: `catalog-info.yaml`) and branch name (default: `backstage-integration`) used in pull requests can be configured in `app-config.yaml` as follows: + +```yaml +// app-config.yaml + +catalog: + import: + entityFilename: anvil.yaml + pullRequestBranchName: anvil-integration +``` + +Following React components have also been updated to accept optional props for providing example entity and repository paths: + +```tsx + +``` + +```tsx + +``` From e9b5f55a8b88adb7d4f589443d3f91c7f88ad40e Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Thu, 20 Jan 2022 11:49:35 +0200 Subject: [PATCH 09/12] chore(catalog-import): generate api report Signed-off-by: Mikko Korhonen --- plugins/catalog-import/api-report.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md index a1ad76f323..a85777c563 100644 --- a/plugins/catalog-import/api-report.md +++ b/plugins/catalog-import/api-report.md @@ -60,7 +60,7 @@ export const AutocompleteTextField: ({ helperText, errorHelperText, textFieldProps, -}: Props_4) => JSX.Element; +}: Props_5) => JSX.Element; // Warning: (ae-missing-release-tag) "CatalogImportApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -171,10 +171,14 @@ export const EntityListComponent: ({ withLinks, }: Props) => JSX.Element; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ImportInfoCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ImportInfoCard: () => JSX.Element; +export const ImportInfoCard: ({ + exampleLocationUrl, + exampleRepositoryUrl, +}: Props_2) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ImportStepper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -184,7 +188,7 @@ export const ImportStepper: ({ initialUrl, generateStepper, variant, -}: Props_2) => JSX.Element; +}: Props_3) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PreparePullRequestForm" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -196,7 +200,7 @@ export const PreparePullRequestForm: < defaultValues, onSubmit, render, -}: Props_5) => JSX.Element; +}: Props_6) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PreviewCatalogInfoComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -206,7 +210,7 @@ export const PreviewCatalogInfoComponent: ({ repositoryUrl, entities, classes, -}: Props_6) => JSX.Element; +}: Props_7) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PreviewPullRequestComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -216,7 +220,7 @@ export const PreviewPullRequestComponent: ({ title, description, classes, -}: Props_7) => JSX.Element; +}: Props_8) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "StepInitAnalyzeUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -226,7 +230,8 @@ export const StepInitAnalyzeUrl: ({ onAnalysis, analysisUrl, disablePullRequest, -}: Props_3) => JSX.Element; + exampleLocationUrl, +}: Props_4) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "StepPrepareCreatePullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -237,7 +242,7 @@ export const StepPrepareCreatePullRequest: ({ onPrepare, onGoBack, renderFormFields, -}: Props_8) => JSX.Element; +}: Props_9) => JSX.Element; // Warnings were encountered during analysis: // From 2734946be07d78d50b5dfcd211019d57442b3148 Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Tue, 25 Jan 2022 09:17:53 +0200 Subject: [PATCH 10/12] test(catalog-import): migrate some tests to use TestApiProvider Signed-off-by: Mikko Korhonen --- .../PreviewCatalogInfoComponent.test.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx index aac54e084a..0a6960cdc7 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx @@ -15,9 +15,8 @@ */ import { Entity } from '@backstage/catalog-model'; -import { ApiProvider } from '@backstage/core-app-api'; import { configApiRef } from '@backstage/core-plugin-api'; -import { TestApiRegistry, MockConfigApi } from '@backstage/test-utils'; +import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; import { makeStyles } from '@material-ui/core'; import { render, screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; @@ -48,18 +47,17 @@ const entities: Entity[] = [ ]; const mockConfigApi = new MockConfigApi({}); - -const apis = TestApiRegistry.from([configApiRef, mockConfigApi]); +const apis = [[configApiRef, mockConfigApi]] as const; describe('', () => { it('renders without exploding', () => { render( - + - , + , ); const repositoryUrl = screen.getByText( @@ -76,13 +74,13 @@ describe('', () => { const { result } = renderHook(() => useStyles()); render( - + - , + , ); const repositoryUrl = screen.getByText( @@ -99,13 +97,13 @@ describe('', () => { const { result } = renderHook(() => useStyles()); render( - + - , + , ); const repositoryUrl = screen.getByText( @@ -120,23 +118,25 @@ describe('', () => { it('renders with custom catalog filename', () => { render( - - , + , ); const repositoryUrl = screen.getByText( From 9735b00b6aa634d6541cf8ca7aa71c2b686f2eec Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Tue, 25 Jan 2022 11:21:53 +0200 Subject: [PATCH 11/12] refactor(catalog-import): access catalog filename and branchname via hooks and helpers Signed-off-by: Mikko Korhonen --- plugins/catalog-import/package.json | 1 + .../src/api/CatalogImportClient.ts | 21 +++++----------- .../ImportInfoCard/ImportInfoCard.tsx | 5 ++-- .../PreviewCatalogInfoComponent.tsx | 7 ++---- .../StepPrepareCreatePullRequest.tsx | 8 +++---- .../catalog-import/src/components/helpers.ts | 15 ++++++++++++ plugins/catalog-import/src/hooks/index.ts | 17 +++++++++++++ .../src/hooks/useCatalogFilename.ts | 24 +++++++++++++++++++ 8 files changed, 70 insertions(+), 28 deletions(-) create mode 100644 plugins/catalog-import/src/hooks/index.ts create mode 100644 plugins/catalog-import/src/hooks/useCatalogFilename.ts diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 5097f5cc9b..2508543541 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -34,6 +34,7 @@ "@backstage/catalog-client": "^0.5.5", "@backstage/catalog-model": "^0.9.10", "@backstage/core-components": "^0.8.6", + "@backstage/config": "^0.1.13", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", "@backstage/integration": "^0.7.2", diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 218814d2fb..60745923b4 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -32,6 +32,7 @@ import { PartialEntity } from '../types'; import { AnalyzeResult, CatalogImportApi } from './CatalogImportApi'; import { getGithubIntegrationConfig } from './GitHub'; import { trimEnd } from 'lodash'; +import { getBranchName, getCatalogFilename } from '../components/helpers'; export class CatalogImportClient implements CatalogImportApi { private readonly discoveryApi: DiscoveryApi; @@ -87,9 +88,7 @@ export class CatalogImportClient implements CatalogImportApi { const ghConfig = getGithubIntegrationConfig(this.scmIntegrationsApi, url); if (!ghConfig) { const other = this.scmIntegrationsApi.byUrl(url); - const catalogFilename = - this.configApi.getOptionalString('catalog.import.entityFilename') ?? - 'catalog-info.yaml'; + const catalogFilename = getCatalogFilename(this.configApi); if (other) { throw new Error( @@ -131,9 +130,7 @@ export class CatalogImportClient implements CatalogImportApi { const appTitle = this.configApi.getOptionalString('app.title') ?? 'Backstage'; const appBaseUrl = this.configApi.getString('app.baseUrl'); - const catalogFilename = - this.configApi.getOptionalString('catalog.import.entityFilename') ?? - 'catalog-info.yaml'; + const catalogFilename = getCatalogFilename(this.configApi); return { title: `Add ${catalogFilename} config file`, @@ -228,9 +225,7 @@ the component will become available.\n\nFor more information, read an \ auth: token, baseUrl: githubIntegrationConfig.apiBaseUrl, }); - const catalogFilename = - this.configApi.getOptionalString('catalog.import.entityFilename') ?? - 'catalog-info.yaml'; + const catalogFilename = getCatalogFilename(this.configApi); const query = `repo:${owner}/${repo}+filename:${catalogFilename}`; const searchResult = await octo.search.code({ q: query }).catch(e => { @@ -303,12 +298,8 @@ the component will become available.\n\nFor more information, read an \ baseUrl: githubIntegrationConfig.apiBaseUrl, }); - const branchName = - this.configApi.getOptionalString('catalog.pullRequestBranchName') ?? - 'backstage-integration'; - const fileName = - this.configApi.getOptionalString('catalog.import.entityFilename') ?? - 'catalog-info.yaml'; + const branchName = getBranchName(this.configApi); + const fileName = getCatalogFilename(this.configApi); const repoData = await octo.repos .get({ diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx index 4d78542e60..3e66e137eb 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx +++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx @@ -19,6 +19,7 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { Chip, Typography } from '@material-ui/core'; import React from 'react'; import { catalogImportApiRef } from '../../api'; +import { useCatalogFilename } from '../../hooks'; type Props = { exampleLocationUrl?: string; @@ -36,9 +37,7 @@ export const ImportInfoCard = ({ const integrations = configApi.getConfig('integrations'); const hasGithubIntegration = integrations.has('github'); - const catalogFilename = - configApi.getOptionalString('catalog.import.entityFilename') ?? - 'catalog-info.yaml'; + const catalogFilename = useCatalogFilename(); return ( { - const configApi = useApi(configApiRef); - const catalogFilename = - configApi.getOptionalString('catalog.import.entityFilename') ?? - 'catalog-info.yaml'; + const catalogFilename = useCatalogFilename(); return ( diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx index d511294b61..a79caaa0b9 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { configApiRef, errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; import { assertError } from '@backstage/errors'; import { catalogApiRef, @@ -28,6 +28,7 @@ import { UnpackNestedValue, UseFormReturn } from 'react-hook-form'; import useAsync from 'react-use/lib/useAsync'; import YAML from 'yaml'; import { AnalyzeResult, catalogImportApiRef } from '../../api'; +import { useCatalogFilename } from '../../hooks'; import { PartialEntity } from '../../types'; import { BackButton, NextButton } from '../Buttons'; import { PrepareResult } from '../useImportState'; @@ -102,14 +103,11 @@ export const StepPrepareCreatePullRequest = ({ const catalogApi = useApi(catalogApiRef); const catalogImportApi = useApi(catalogImportApiRef); const errorApi = useApi(errorApiRef); - const configApi = useApi(configApiRef); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(); - const catalogFilename = - configApi.getOptionalString('catalog.import.entityFilename') ?? - 'catalog-info.yaml'; + const catalogFilename = useCatalogFilename(); const { loading: prDefaultsLoading, diff --git a/plugins/catalog-import/src/components/helpers.ts b/plugins/catalog-import/src/components/helpers.ts index 4240186459..b134a8726a 100644 --- a/plugins/catalog-import/src/components/helpers.ts +++ b/plugins/catalog-import/src/components/helpers.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import type { Config } from '@backstage/config'; import { UseFormRegisterReturn } from 'react-hook-form'; /** @@ -31,3 +32,17 @@ export function asInputRef(renderResult: UseFormRegisterReturn) { ...rest, }; } + +export function getCatalogFilename(config: Config): string { + return ( + config.getOptionalString('catalog.import.entityFilename') ?? + 'catalog-info.yaml' + ); +} + +export function getBranchName(config: Config): string { + return ( + config.getOptionalString('catalog.import.pullRequestBranchName') ?? + 'backstage-integration' + ); +} diff --git a/plugins/catalog-import/src/hooks/index.ts b/plugins/catalog-import/src/hooks/index.ts new file mode 100644 index 0000000000..afdfb0cbe2 --- /dev/null +++ b/plugins/catalog-import/src/hooks/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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. + */ + +export { useCatalogFilename } from './useCatalogFilename'; diff --git a/plugins/catalog-import/src/hooks/useCatalogFilename.ts b/plugins/catalog-import/src/hooks/useCatalogFilename.ts new file mode 100644 index 0000000000..0e2d770d3e --- /dev/null +++ b/plugins/catalog-import/src/hooks/useCatalogFilename.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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 { useApi, configApiRef } from '@backstage/core-plugin-api'; +import { getCatalogFilename } from '../components/helpers'; + +export function useCatalogFilename(): string { + const config = useApi(configApiRef); + + return getCatalogFilename(config); +} From bee418a36b7db3d8ebdfa82e2d43cede76e8a11d Mon Sep 17 00:00:00 2001 From: Mikko Korhonen Date: Tue, 25 Jan 2022 11:36:18 +0200 Subject: [PATCH 12/12] docs(catalog-import): update documentation about catalog filename customizations Signed-off-by: Mikko Korhonen --- plugins/catalog-import/README.md | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md index f492619690..86f3aecba9 100644 --- a/plugins/catalog-import/README.md +++ b/plugins/catalog-import/README.md @@ -35,6 +35,8 @@ import { CatalogImportPage } from '@backstage/plugin-catalog-import'; ## Customizations +### Custom layout + A custom layout can be passed to the import page, as it's already supported by the search page. If no custom layout is passed, the default layout is used. @@ -72,6 +74,37 @@ feature by passing options to `` (`pullRequest.disable` and function. The function can either be overridden to generate a different content for the pull request, or removed to disable this feature. +### Entity filename and branch name + +Entity filename (default: `catalog-info.yaml`) and branch name (default: `backstage-integration`) used in pull requests can be configured in `app-config.yaml` as follows: + +```yaml +// app-config.yaml + +catalog: + import: + entityFilename: anvil.yaml + pullRequestBranchName: anvil-integration +``` + +### Entity examples + +Following React components accept optional props for providing custom example entity and repository paths: + +```tsx + +``` + +```tsx + +``` + ## Development Use `yarn start` to run a [development version](./dev/index.tsx) of the plugin that can be used to validate each flow with mocked data.