Merge pull request #8934 from kmikko/feat/catalog-import-customizations
feat(catalog-import): Make catalog filename and branch customizable
This commit is contained in:
@@ -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
|
||||
<StepInitAnalyzeUrl
|
||||
...
|
||||
exampleLocationUrl="https://github.com/acme-corp/our-awesome-api/blob/main/anvil.yaml"
|
||||
/>
|
||||
```
|
||||
|
||||
```tsx
|
||||
<ImportInfoCard
|
||||
exampleLocationUrl="https://github.com/acme-corp/our-awesome-api/blob/main/anvil.yaml"
|
||||
exampleRepositoryUrl="https://github.com/acme-corp/our-awesome-api"
|
||||
/>
|
||||
```
|
||||
@@ -194,6 +194,9 @@ integrations:
|
||||
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
|
||||
|
||||
catalog:
|
||||
import:
|
||||
entityFilename: catalog-info.yaml
|
||||
pullRequestBranchName: backstage-integration
|
||||
rules:
|
||||
- allow:
|
||||
- Component
|
||||
|
||||
@@ -80,6 +80,9 @@ scaffolder:
|
||||
# see https://backstage.io/docs/features/software-templates/configuration for software template options
|
||||
|
||||
catalog:
|
||||
import:
|
||||
entityFilename: catalog-info.yaml
|
||||
pullRequestBranchName: backstage-integration
|
||||
rules:
|
||||
- allow: [Component, System, API, Group, User, Resource, Location]
|
||||
locations:
|
||||
|
||||
@@ -35,39 +35,73 @@ import { CatalogImportPage } from '@backstage/plugin-catalog-import';
|
||||
|
||||
## Customizations
|
||||
|
||||
### Disable the creation of Pull Requests
|
||||
### Custom layout
|
||||
|
||||
The pull request feature can be disabled by options that are passed to the `CatalogImportPage`:
|
||||
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.
|
||||
|
||||
```typescript
|
||||
<Route path="/catalog-import" element={<CatalogImportPage />}>
|
||||
<Page themeId="home">
|
||||
<Header title="Register an existing component" />
|
||||
<Content>
|
||||
<ContentHeader title="Start tracking your components">
|
||||
<SupportButton>
|
||||
Start tracking your component in Backstage by adding it to the
|
||||
software catalog.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
|
||||
<Grid container spacing={2} direction="row-reverse">
|
||||
<Grid item xs={12} md={4} lg={6} xl={8}>
|
||||
Hello World
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={8} lg={6} xl={4}>
|
||||
<ImportStepper />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
</Route>
|
||||
```
|
||||
|
||||
Previously it was possible to disable and customize the automatic pull request
|
||||
feature by passing options to `<CatalogImportPage>` (`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.
|
||||
|
||||
### 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
|
||||
// packages/app/src/App.tsx
|
||||
|
||||
<Route
|
||||
path="/catalog-import"
|
||||
element={<CatalogImportPage pullRequest={{ disable: true }} />}
|
||||
<StepInitAnalyzeUrl
|
||||
...
|
||||
exampleLocationUrl="https://github.com/acme-corp/our-awesome-api/blob/main/anvil.yaml"
|
||||
/>
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
<Route
|
||||
path="/catalog-import"
|
||||
element={
|
||||
<CatalogImportPage
|
||||
pullRequest={{
|
||||
preparePullRequest: () => ({
|
||||
title: 'My title',
|
||||
body: 'My **markdown** body',
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<ImportInfoCard
|
||||
exampleLocationUrl="https://github.com/acme-corp/our-awesome-api/blob/main/anvil.yaml"
|
||||
exampleRepositoryUrl="https://github.com/acme-corp/our-awesome-api"
|
||||
/>
|
||||
```
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export const AutocompleteTextField: <TFieldValue extends string>({
|
||||
helperText,
|
||||
errorHelperText,
|
||||
textFieldProps,
|
||||
}: Props_4<TFieldValue>) => JSX.Element;
|
||||
}: Props_5<TFieldValue>) => 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<TFieldValues>) => JSX.Element;
|
||||
}: Props_6<TFieldValues>) => 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:
|
||||
//
|
||||
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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;
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
@@ -69,6 +70,8 @@
|
||||
"msw": "^0.35.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.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),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,13 +88,15 @@ export class CatalogImportClient implements CatalogImportApi {
|
||||
const ghConfig = getGithubIntegrationConfig(this.scmIntegrationsApi, url);
|
||||
if (!ghConfig) {
|
||||
const other = this.scmIntegrationsApi.byUrl(url);
|
||||
const catalogFilename = getCatalogFilename(this.configApi);
|
||||
|
||||
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 +130,10 @@ export class CatalogImportClient implements CatalogImportApi {
|
||||
const appTitle =
|
||||
this.configApi.getOptionalString('app.title') ?? 'Backstage';
|
||||
const appBaseUrl = this.configApi.getString('app.baseUrl');
|
||||
const catalogFilename = getCatalogFilename(this.configApi);
|
||||
|
||||
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 +225,8 @@ 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 = getCatalogFilename(this.configApi);
|
||||
const query = `repo:${owner}/${repo}+filename:${catalogFilename}`;
|
||||
|
||||
const searchResult = await octo.search.code({ q: query }).catch(e => {
|
||||
throw new Error(
|
||||
@@ -294,8 +298,8 @@ the component will become available.\n\nFor more information, read an \
|
||||
baseUrl: githubIntegrationConfig.apiBaseUrl,
|
||||
});
|
||||
|
||||
const branchName = 'backstage-integration';
|
||||
const fileName = 'catalog-info.yaml';
|
||||
const branchName = getBranchName(this.configApi);
|
||||
const fileName = getCatalogFilename(this.configApi);
|
||||
|
||||
const repoData = await octo.repos
|
||||
.get({
|
||||
|
||||
@@ -19,8 +19,17 @@ 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';
|
||||
|
||||
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);
|
||||
@@ -28,6 +37,8 @@ export const ImportInfoCard = () => {
|
||||
const integrations = configApi.getConfig('integrations');
|
||||
const hasGithubIntegration = integrations.has('github');
|
||||
|
||||
const catalogFilename = useCatalogFilename();
|
||||
|
||||
return (
|
||||
<InfoCard
|
||||
title="Register an existing component"
|
||||
@@ -41,10 +52,7 @@ export const ImportInfoCard = () => {
|
||||
</Typography>
|
||||
<Typography variant="h6">Link to an existing entity file</Typography>
|
||||
<Typography variant="subtitle2" color="textSecondary" paragraph>
|
||||
Example:{' '}
|
||||
<code>
|
||||
https://github.com/backstage/backstage/blob/master/catalog-info.yaml
|
||||
</code>
|
||||
Example: <code>{exampleLocationUrl}</code>
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
The wizard analyzes the file, previews the entities, and adds them to
|
||||
@@ -57,17 +65,17 @@ export const ImportInfoCard = () => {
|
||||
<Chip label="GitHub only" variant="outlined" size="small" />
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" color="textSecondary" paragraph>
|
||||
Example: <code>https://github.com/backstage/backstage</code>
|
||||
Example: <code>{exampleRepositoryUrl}</code>
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
The wizard discovers all <code>catalog-info.yaml</code> files in the
|
||||
The wizard discovers all <code>{catalogFilename}</code> files in the
|
||||
repository, previews the entities, and adds them to the {appTitle}{' '}
|
||||
catalog.
|
||||
</Typography>
|
||||
{catalogImportApi.preparePullRequest && (
|
||||
<Typography variant="body2" paragraph>
|
||||
If no entities are found, the wizard will prepare a Pull Request
|
||||
that adds an example <code>catalog-info.yaml</code> and prepares
|
||||
that adds an example <code>{catalogFilename}</code> and prepares
|
||||
the {appTitle} catalog to load all entities as soon as the Pull
|
||||
Request is merged.
|
||||
</Typography>
|
||||
|
||||
@@ -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"
|
||||
|
||||
+55
-14
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
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';
|
||||
@@ -44,13 +46,18 @@ const entities: Entity[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const mockConfigApi = new MockConfigApi({});
|
||||
const apis = [[configApiRef, mockConfigApi]] as const;
|
||||
|
||||
describe('<PreviewCatalogInfoComponent />', () => {
|
||||
it('renders without exploding', () => {
|
||||
render(
|
||||
<PreviewCatalogInfoComponent
|
||||
repositoryUrl="http://my-repository/a/"
|
||||
entities={entities}
|
||||
/>,
|
||||
<TestApiProvider apis={apis}>
|
||||
<PreviewCatalogInfoComponent
|
||||
repositoryUrl="http://my-repository/a/"
|
||||
entities={entities}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
const repositoryUrl = screen.getByText(
|
||||
@@ -67,11 +74,13 @@ describe('<PreviewCatalogInfoComponent />', () => {
|
||||
const { result } = renderHook(() => useStyles());
|
||||
|
||||
render(
|
||||
<PreviewCatalogInfoComponent
|
||||
repositoryUrl="http://my-repository/a/"
|
||||
entities={entities}
|
||||
classes={{ card: result.current.displayNone }}
|
||||
/>,
|
||||
<TestApiProvider apis={apis}>
|
||||
<PreviewCatalogInfoComponent
|
||||
repositoryUrl="http://my-repository/a/"
|
||||
entities={entities}
|
||||
classes={{ card: result.current.displayNone }}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
const repositoryUrl = screen.getByText(
|
||||
@@ -88,11 +97,13 @@ describe('<PreviewCatalogInfoComponent />', () => {
|
||||
const { result } = renderHook(() => useStyles());
|
||||
|
||||
render(
|
||||
<PreviewCatalogInfoComponent
|
||||
repositoryUrl="http://my-repository/a/"
|
||||
entities={entities}
|
||||
classes={{ cardContent: result.current.displayNone }}
|
||||
/>,
|
||||
<TestApiProvider apis={apis}>
|
||||
<PreviewCatalogInfoComponent
|
||||
repositoryUrl="http://my-repository/a/"
|
||||
entities={entities}
|
||||
classes={{ cardContent: result.current.displayNone }}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
const repositoryUrl = screen.getByText(
|
||||
@@ -104,4 +115,34 @@ describe('<PreviewCatalogInfoComponent />', () => {
|
||||
expect(kindText).toBeInTheDocument();
|
||||
expect(kindText).not.toBeVisible();
|
||||
});
|
||||
|
||||
it('renders with custom catalog filename', () => {
|
||||
render(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
configApiRef,
|
||||
new MockConfigApi({
|
||||
catalog: {
|
||||
import: {
|
||||
entityFilename: 'anvil.yaml',
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
]}
|
||||
>
|
||||
<PreviewCatalogInfoComponent
|
||||
repositoryUrl="http://acme-corp/awesome-api/"
|
||||
entities={entities}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
const repositoryUrl = screen.getByText(
|
||||
'http://acme-corp/awesome-api/anvil.yaml',
|
||||
);
|
||||
expect(repositoryUrl).toBeInTheDocument();
|
||||
expect(repositoryUrl).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
+4
-1
@@ -20,6 +20,7 @@ import React from 'react';
|
||||
import YAML from 'yaml';
|
||||
import { CodeSnippet } from '@backstage/core-components';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { useCatalogFilename } from '../../hooks';
|
||||
|
||||
type Props = {
|
||||
repositoryUrl: string;
|
||||
@@ -32,11 +33,13 @@ export const PreviewCatalogInfoComponent = ({
|
||||
entities,
|
||||
classes,
|
||||
}: Props) => {
|
||||
const catalogFilename = useCatalogFilename();
|
||||
|
||||
return (
|
||||
<Card variant="outlined" className={classes?.card}>
|
||||
<CardHeader
|
||||
title={
|
||||
<code>{`${trimEnd(repositoryUrl, '/')}/catalog-info.yaml`}</code>
|
||||
<code>{`${trimEnd(repositoryUrl, '/')}/${catalogFilename}`}</code>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
+5
-2
@@ -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('<StepPrepareCreatePullRequest />', () => {
|
||||
post: jest.fn(),
|
||||
};
|
||||
|
||||
const configApi = new MockConfigApi({});
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogImportApiRef, catalogImportApi],
|
||||
[catalogApiRef, catalogApi],
|
||||
[errorApiRef, errorApi],
|
||||
[configApiRef, configApi],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
|
||||
+4
-1
@@ -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';
|
||||
@@ -106,6 +107,8 @@ export const StepPrepareCreatePullRequest = ({
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const catalogFilename = useCatalogFilename();
|
||||
|
||||
const {
|
||||
loading: prDefaultsLoading,
|
||||
value: prDefaults,
|
||||
@@ -193,7 +196,7 @@ export const StepPrepareCreatePullRequest = ({
|
||||
<>
|
||||
<Typography>
|
||||
You entered a link to a {analyzeResult.integrationType} repository but a{' '}
|
||||
<code>catalog-info.yaml</code> could not be found. Use this form to open
|
||||
<code>{catalogFilename}</code> could not be found. Use this form to open
|
||||
a Pull Request that creates one.
|
||||
</Typography>
|
||||
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user