diff --git a/.changeset/few-penguins-watch.md b/.changeset/few-penguins-watch.md
new file mode 100644
index 0000000000..3d7d9fb457
--- /dev/null
+++ b/.changeset/few-penguins-watch.md
@@ -0,0 +1,45 @@
+---
+'@backstage/plugin-catalog-import': minor
+---
+
+Add initial support for customizing the catalog import page.
+
+It is now possible to pass a custom layout 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
+}>
+
+
+
+
+
+ Start tracking your component in Backstage by adding it to the
+ software catalog.
+
+
+
+
+
+ Hello World
+
+
+
+
+
+
+
+
+
+```
+
+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.
+
+The export of the long term deprecated legacy `` is removed, migrate to
+`` instead.
diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md
index 6752f95748..753244b28f 100644
--- a/plugins/catalog-import/api-report.md
+++ b/plugins/catalog-import/api-report.md
@@ -68,6 +68,11 @@ export interface CatalogImportApi {
// (undocumented)
analyzeUrl(url: string): Promise;
// (undocumented)
+ preparePullRequest?(): Promise<{
+ title: string;
+ body: string;
+ }>;
+ // (undocumented)
submitPullRequest(options: {
repositoryUrl: string;
fileContent: string;
@@ -94,10 +99,16 @@ export class CatalogImportClient implements CatalogImportApi {
identityApi: IdentityApi;
scmIntegrationsApi: ScmIntegrationRegistry;
catalogApi: CatalogApi;
+ configApi: ConfigApi;
});
// (undocumented)
analyzeUrl(url: string): Promise;
// (undocumented)
+ preparePullRequest(): Promise<{
+ title: string;
+ body: string;
+ }>;
+ // (undocumented)
submitPullRequest({
repositoryUrl,
fileContent,
@@ -114,11 +125,10 @@ export class CatalogImportClient implements CatalogImportApi {
}>;
}
-// Warning: (ae-forgotten-export) The symbol "StepperProviderOpts" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "CatalogImportPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export const CatalogImportPage: (opts: StepperProviderOpts) => JSX.Element;
+export const CatalogImportPage: () => JSX.Element;
// Warning: (ae-missing-release-tag) "catalogImportPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -144,6 +154,11 @@ export function defaultGenerateStepper(
defaults: StepperProvider,
): StepperProvider;
+// Warning: (ae-missing-release-tag) "DefaultImportPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const DefaultImportPage: () => 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) "EntityListComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -155,7 +170,12 @@ export const EntityListComponent: ({
onItemClick,
firstListItem,
withLinks,
-}: Props_2) => JSX.Element;
+}: Props) => JSX.Element;
+
+// 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;
// 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)
@@ -165,8 +185,7 @@ export const ImportStepper: ({
initialUrl,
generateStepper,
variant,
- opts,
-}: Props) => JSX.Element;
+}: Props_2) => JSX.Element;
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
@@ -203,11 +222,6 @@ export const PreviewPullRequestComponent: ({
classes,
}: Props_7) => JSX.Element;
-// Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
-//
-// @public (undocumented)
-export const Router: (opts: StepperProviderOpts) => JSX.Element;
-
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
@@ -230,8 +244,6 @@ export const StepPrepareCreatePullRequest: ({
onPrepare,
onGoBack,
renderFormFields,
- defaultTitle,
- defaultBody,
}: Props_8) => JSX.Element;
// Warnings were encountered during analysis:
diff --git a/plugins/catalog-import/dev/index.tsx b/plugins/catalog-import/dev/index.tsx
index 48d9320ca9..a0cf034782 100644
--- a/plugins/catalog-import/dev/index.tsx
+++ b/plugins/catalog-import/dev/index.tsx
@@ -29,7 +29,7 @@ import {
EntityListComponent,
ImportStepper,
} from '../src';
-import { ImportComponentPage } from '../src/components/ImportComponentPage';
+import { ImportPage } from '../src/components/ImportPage';
import { Content, Header, InfoCard, Page } from '@backstage/core-components';
const getEntityNames = (url: string): EntityName[] => [
@@ -252,7 +252,7 @@ createDevApp()
})
.addPage({
title: 'Catalog Import',
- element: ,
+ element: ,
})
.addPage({
title: 'Catalog Import 2',
diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts
index 04e98f3d7c..a4645dc7ef 100644
--- a/plugins/catalog-import/src/api/CatalogImportApi.ts
+++ b/plugins/catalog-import/src/api/CatalogImportApi.ts
@@ -15,8 +15,8 @@
*/
import { EntityName } from '@backstage/catalog-model';
-import { PartialEntity } from '../types';
import { createApiRef } from '@backstage/core-plugin-api';
+import { PartialEntity } from '../types';
export const catalogImportApiRef = createApiRef({
id: 'plugin.catalog-import.service',
@@ -42,6 +42,10 @@ export type AnalyzeResult =
export interface CatalogImportApi {
analyzeUrl(url: string): Promise;
+ preparePullRequest?(): Promise<{
+ title: string;
+ body: string;
+ }>;
submitPullRequest(options: {
repositoryUrl: string;
fileContent: string;
diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts
index 2936f956e4..0782b44147 100644
--- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts
+++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts
@@ -115,6 +115,11 @@ describe('CatalogImportClient', () => {
scmIntegrationsApi,
identityApi,
catalogApi,
+ configApi: new ConfigReader({
+ app: {
+ baseUrl: 'https://demo.backstage.io/',
+ },
+ }),
});
});
@@ -443,4 +448,13 @@ describe('CatalogImportClient', () => {
});
});
});
+
+ describe('preparePullRequest', () => {
+ test('should prepare pull request details', async () => {
+ await expect(catalogImportClient.preparePullRequest()).resolves.toEqual({
+ title: 'Add catalog-info.yaml config file',
+ body: expect.any(String),
+ });
+ });
+ });
});
diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts
index 2497af99d8..242f8440d1 100644
--- a/plugins/catalog-import/src/api/CatalogImportClient.ts
+++ b/plugins/catalog-import/src/api/CatalogImportClient.ts
@@ -17,6 +17,7 @@
import { CatalogApi } from '@backstage/catalog-client';
import { EntityName } from '@backstage/catalog-model';
import {
+ ConfigApi,
DiscoveryApi,
IdentityApi,
OAuthApi,
@@ -37,6 +38,7 @@ export class CatalogImportClient implements CatalogImportApi {
private readonly githubAuthApi: OAuthApi;
private readonly scmIntegrationsApi: ScmIntegrationRegistry;
private readonly catalogApi: CatalogApi;
+ private readonly configApi: ConfigApi;
constructor(options: {
discoveryApi: DiscoveryApi;
@@ -44,12 +46,14 @@ export class CatalogImportClient implements CatalogImportApi {
identityApi: IdentityApi;
scmIntegrationsApi: ScmIntegrationRegistry;
catalogApi: CatalogApi;
+ configApi: ConfigApi;
}) {
this.discoveryApi = options.discoveryApi;
this.githubAuthApi = options.githubAuthApi;
this.identityApi = options.identityApi;
this.scmIntegrationsApi = options.scmIntegrationsApi;
this.catalogApi = options.catalogApi;
+ this.configApi = options.configApi;
}
async analyzeUrl(url: string): Promise {
@@ -114,6 +118,24 @@ export class CatalogImportClient implements CatalogImportApi {
};
}
+ async preparePullRequest(): Promise<{
+ title: string;
+ body: string;
+ }> {
+ const appTitle =
+ this.configApi.getOptionalString('app.title') ?? 'Backstage';
+ const appBaseUrl = this.configApi.getString('app.baseUrl');
+
+ return {
+ title: 'Add catalog-info.yaml 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, \
+the component will become available.\n\nFor more information, read an \
+[overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview).`,
+ };
+ }
+
async submitPullRequest({
repositoryUrl,
fileContent,
diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx
similarity index 74%
rename from plugins/catalog-import/src/components/ImportComponentPage.test.tsx
rename to plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx
index 2e572c8936..57376c33f2 100644
--- a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx
+++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx
@@ -15,21 +15,19 @@
*/
import { CatalogClient } from '@backstage/catalog-client';
-import { catalogApiRef } from '@backstage/plugin-catalog-react';
-import { wrapInTestApp } from '@backstage/test-utils';
-import { act, render } from '@testing-library/react';
-import React from 'react';
-import { catalogImportApiRef, CatalogImportClient } from '../api';
-import { ImportComponentPage } from './ImportComponentPage';
-
import {
ApiProvider,
ApiRegistry,
ConfigReader,
} from '@backstage/core-app-api';
import { configApiRef } from '@backstage/core-plugin-api';
+import { catalogApiRef } from '@backstage/plugin-catalog-react';
+import { renderInTestApp } from '@backstage/test-utils';
+import React from 'react';
+import { catalogImportApiRef, CatalogImportClient } from '../../api';
+import { DefaultImportPage } from './DefaultImportPage';
-describe('', () => {
+describe('', () => {
const identityApi = {
getUserId: () => {
return 'user';
@@ -63,23 +61,20 @@ describe('', () => {
identityApi,
scmIntegrationsApi: {} as any,
catalogApi: {} as any,
+ configApi: {} as any,
}),
);
});
it('renders without exploding', async () => {
- await act(async () => {
- const { getByText } = render(
- wrapInTestApp(
-
-
- ,
- ),
- );
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
- expect(
- await getByText('Start tracking your component in Backstage'),
- ).toBeInTheDocument();
- });
+ expect(
+ getByText('Start tracking your component in Backstage'),
+ ).toBeInTheDocument();
});
});
diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx
new file mode 100644
index 0000000000..8693895ec0
--- /dev/null
+++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 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 {
+ Content,
+ ContentHeader,
+ Header,
+ Page,
+ SupportButton,
+} from '@backstage/core-components';
+import { configApiRef, useApi } from '@backstage/core-plugin-api';
+import { Grid } from '@material-ui/core';
+import React from 'react';
+import { ImportInfoCard } from '../ImportInfoCard';
+import { ImportStepper } from '../ImportStepper';
+
+export const DefaultImportPage = () => {
+ const configApi = useApi(configApiRef);
+ const appTitle = configApi.getOptional('app.title') || 'Backstage';
+
+ return (
+
+
+
+
+
+ Start tracking your component in {appTitle} by adding it to the
+ software catalog.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/catalog-import/src/components/DefaultImportPage/index.ts b/plugins/catalog-import/src/components/DefaultImportPage/index.ts
new file mode 100644
index 0000000000..5a4d4907e3
--- /dev/null
+++ b/plugins/catalog-import/src/components/DefaultImportPage/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 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 { DefaultImportPage } from './DefaultImportPage';
diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx
deleted file mode 100644
index bf53af134f..0000000000
--- a/plugins/catalog-import/src/components/ImportComponentPage.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright 2020 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 { Chip, Grid, Typography } from '@material-ui/core';
-import React from 'react';
-import { ImportStepper } from './ImportStepper';
-import { StepperProviderOpts } from './ImportStepper/defaults';
-
-import { configApiRef, useApi } from '@backstage/core-plugin-api';
-import {
- Content,
- ContentHeader,
- Header,
- InfoCard,
- Page,
- SupportButton,
-} from '@backstage/core-components';
-
-export const ImportComponentPage = (opts: StepperProviderOpts) => {
- const configApi = useApi(configApiRef);
- const appTitle = configApi.getOptional('app.title') || 'Backstage';
-
- const integrations = configApi.getConfig('integrations');
- const hasGithubIntegration = integrations.has('github');
-
- return (
-
-
-
-
-
- Start tracking your component in {appTitle} by adding it to the
- software catalog.
-
-
-
-
-
-
-
- Enter the URL to your source code repository to add it to{' '}
- {appTitle}.
-
-
- Link to an existing entity file
-
-
- Example:{' '}
-
- https://github.com/backstage/backstage/blob/master/catalog-info.yaml
-
-
-
- The wizard analyzes the file, previews the entities, and adds
- them to the {appTitle} catalog.
-
- {hasGithubIntegration && (
- <>
-
- Link to a repository{' '}
-
-
-
- Example: https://github.com/backstage/backstage
-
-
- The wizard discovers all catalog-info.yaml{' '}
- files in the repository, previews the entities, and adds
- them to the {appTitle} catalog.
-
- {!opts?.pullRequest?.disable && (
-
- If no entities are found, the wizard will prepare a Pull
- Request that adds an example{' '}
- catalog-info.yaml 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/ImportInfoCard/ImportInfoCard.test.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx
new file mode 100644
index 0000000000..4e0c3694cd
--- /dev/null
+++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021 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 {
+ ApiProvider,
+ ApiRegistry,
+ ConfigReader,
+} from '@backstage/core-app-api';
+import { configApiRef } from '@backstage/core-plugin-api';
+import { renderInTestApp } from '@backstage/test-utils';
+import React from 'react';
+import { CatalogImportApi, catalogImportApiRef } from '../../api';
+import { ImportInfoCard } from './ImportInfoCard';
+
+describe('', () => {
+ let apis: ApiRegistry;
+ let catalogImportApi: jest.Mocked;
+
+ beforeEach(() => {
+ catalogImportApi = {
+ analyzeUrl: jest.fn(),
+ submitPullRequest: jest.fn(),
+ };
+
+ apis = ApiRegistry.with(
+ configApiRef,
+ new ConfigReader({
+ integrations: {
+ github: [{ token: 'my-token' }],
+ },
+ }),
+ ).with(catalogImportApiRef, catalogImportApi);
+ });
+
+ it('renders without exploding', async () => {
+ apis = ApiRegistry.with(
+ configApiRef,
+ new ConfigReader({ integrations: {} }),
+ ).with(catalogImportApiRef, catalogImportApi);
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(getByText('Register an existing component')).toBeInTheDocument();
+ });
+
+ it('renders section on GitHub discovery if supported', async () => {
+ catalogImportApi.preparePullRequest = async () => ({ title: '', body: '' });
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(getByText(/The wizard discovers all/)).toBeInTheDocument();
+ });
+
+ it('renders section on pull requests if supported', async () => {
+ catalogImportApi.preparePullRequest = async () => ({ title: '', body: '' });
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(
+ getByText(/the wizard will prepare a Pull Request/),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx
new file mode 100644
index 0000000000..2fab223f20
--- /dev/null
+++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2021 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 { InfoCard } from '@backstage/core-components';
+import { configApiRef, useApi } from '@backstage/core-plugin-api';
+import { Chip, Typography } from '@material-ui/core';
+import React from 'react';
+import { catalogImportApiRef } from '../../api';
+
+export const ImportInfoCard = () => {
+ const configApi = useApi(configApiRef);
+ const appTitle = configApi.getOptional('app.title') || 'Backstage';
+ const catalogImportApi = useApi(catalogImportApiRef);
+
+ const integrations = configApi.getConfig('integrations');
+ const hasGithubIntegration = integrations.has('github');
+
+ return (
+
+
+ Enter the URL to your source code repository to add it to {appTitle}.
+
+ Link to an existing entity file
+
+ Example:{' '}
+
+ https://github.com/backstage/backstage/blob/master/catalog-info.yaml
+
+
+
+ The wizard analyzes the file, previews the entities, and adds them to
+ the {appTitle} catalog.
+
+ {hasGithubIntegration && (
+ <>
+
+ Link to a repository{' '}
+
+
+
+ Example: https://github.com/backstage/backstage
+
+
+ The wizard discovers all catalog-info.yaml 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
+ the {appTitle} catalog to load all entities as soon as the Pull
+ Request is merged.
+
+ )}
+ >
+ )}
+
+ );
+};
diff --git a/plugins/catalog-import/src/components/ImportInfoCard/index.ts b/plugins/catalog-import/src/components/ImportInfoCard/index.ts
new file mode 100644
index 0000000000..c82e88f7e8
--- /dev/null
+++ b/plugins/catalog-import/src/components/ImportInfoCard/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 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 { ImportInfoCard } from './ImportInfoCard';
diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx
new file mode 100644
index 0000000000..fe3c3673f8
--- /dev/null
+++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 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 { CatalogClient } from '@backstage/catalog-client';
+import {
+ ApiProvider,
+ ApiRegistry,
+ ConfigReader,
+} from '@backstage/core-app-api';
+import { configApiRef } from '@backstage/core-plugin-api';
+import { catalogApiRef } from '@backstage/plugin-catalog-react';
+import { renderInTestApp } from '@backstage/test-utils';
+import React from 'react';
+import { useOutlet } from 'react-router';
+import { catalogImportApiRef, CatalogImportClient } from '../../api';
+import { ImportPage } from './ImportPage';
+
+jest.mock('react-router', () => ({
+ ...jest.requireActual('react-router'),
+ useOutlet: jest.fn(),
+}));
+
+describe('', () => {
+ const identityApi = {
+ getUserId: () => {
+ return 'user';
+ },
+ getProfile: () => {
+ return {};
+ },
+ getIdToken: () => {
+ return Promise.resolve('token');
+ },
+ signOut: () => {
+ return Promise.resolve();
+ },
+ };
+
+ let apis: ApiRegistry;
+
+ beforeEach(() => {
+ apis = ApiRegistry.with(
+ configApiRef,
+ new ConfigReader({ integrations: {} }),
+ )
+ .with(catalogApiRef, new CatalogClient({ discoveryApi: {} as any }))
+ .with(
+ catalogImportApiRef,
+ new CatalogImportClient({
+ discoveryApi: {} as any,
+ githubAuthApi: {
+ getAccessToken: async () => 'token',
+ },
+ identityApi,
+ scmIntegrationsApi: {} as any,
+ catalogApi: {} as any,
+ configApi: new ConfigReader({}),
+ }),
+ );
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('renders without exploding', async () => {
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(
+ getByText('Start tracking your component in Backstage'),
+ ).toBeInTheDocument();
+ });
+
+ it('renders with custom children', async () => {
+ (useOutlet as jest.Mock).mockReturnValue(Hello World
);
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(getByText('Hello World')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/catalog-import/src/components/Router.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx
similarity index 66%
rename from plugins/catalog-import/src/components/Router.tsx
rename to plugins/catalog-import/src/components/ImportPage/ImportPage.tsx
index c8f1ae65a3..467bd710f0 100644
--- a/plugins/catalog-import/src/components/Router.tsx
+++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx
@@ -15,12 +15,11 @@
*/
import React from 'react';
-import { Route, Routes } from 'react-router-dom';
-import { ImportComponentPage } from './ImportComponentPage';
-import { StepperProviderOpts } from './ImportStepper/defaults';
+import { useOutlet } from 'react-router';
+import { DefaultImportPage } from '../DefaultImportPage';
-export const Router = (opts: StepperProviderOpts) => (
-
- } />
-
-);
+export const ImportPage = () => {
+ const outlet = useOutlet();
+
+ return outlet || ;
+};
diff --git a/plugins/catalog-import/src/components/ImportPage/index.ts b/plugins/catalog-import/src/components/ImportPage/index.ts
new file mode 100644
index 0000000000..891c5302db
--- /dev/null
+++ b/plugins/catalog-import/src/components/ImportPage/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 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 { ImportPage } from './ImportPage';
diff --git a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx
index 48ee23e444..4112775c2d 100644
--- a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx
+++ b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx
@@ -14,21 +14,20 @@
* limitations under the License.
*/
+import { InfoCard, InfoCardVariants } from '@backstage/core-components';
+import { useApi } from '@backstage/core-plugin-api';
import { Step, StepContent, Stepper } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import React, { useMemo } from 'react';
+import { catalogImportApiRef } from '../../api';
import { ImportFlows, ImportState, useImportState } from '../useImportState';
import {
defaultGenerateStepper,
defaultStepper,
StepConfiguration,
StepperProvider,
- StepperProviderOpts,
} from './defaults';
-import { configApiRef, useApi } from '@backstage/core-plugin-api';
-import { InfoCard, InfoCardVariants } from '@backstage/core-components';
-
const useStyles = makeStyles(() => ({
stepperRoot: {
padding: 0,
@@ -42,16 +41,14 @@ type Props = {
defaults: StepperProvider,
) => StepperProvider;
variant?: InfoCardVariants;
- opts?: StepperProviderOpts;
};
export const ImportStepper = ({
initialUrl,
generateStepper = defaultGenerateStepper,
variant,
- opts,
}: Props) => {
- const configApi = useApi(configApiRef);
+ const catalogImportApi = useApi(catalogImportApiRef);
const classes = useStyles();
const state = useImportState({ initialUrl });
@@ -79,25 +76,25 @@ export const ImportStepper = ({
{render(
states.analyze(
state as Extract,
- { apis: { configApi }, opts },
+ { apis: { catalogImportApi } },
),
)}
{render(
states.prepare(
state as Extract,
- { apis: { configApi }, opts },
+ { apis: { catalogImportApi } },
),
)}
{render(
states.review(
state as Extract,
- { apis: { configApi }, opts },
+ { apis: { catalogImportApi } },
),
)}
{render(
states.finish(
state as Extract,
- { apis: { configApi }, opts },
+ { apis: { catalogImportApi } },
),
)}
diff --git a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx
index 79d859ce32..357ed64e14 100644
--- a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx
+++ b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-import { ConfigApi } from '@backstage/core-plugin-api';
import {
Box,
Checkbox,
@@ -35,22 +34,9 @@ import {
} from '../StepPrepareCreatePullRequest';
import { StepPrepareSelectLocations } from '../StepPrepareSelectLocations';
import { StepReviewLocation } from '../StepReviewLocation';
+import { StepperApis } from '../types';
import { ImportFlows, ImportState } from '../useImportState';
-export type StepperProviderOpts = {
- pullRequest?: {
- disable?: boolean;
- preparePullRequest?: (apis: StepperApis) => {
- title?: string;
- body?: string;
- };
- };
-};
-
-type StepperApis = {
- configApi: ConfigApi;
-};
-
export type StepConfiguration = {
stepLabel: React.ReactElement;
content: React.ReactElement;
@@ -59,41 +45,22 @@ export type StepConfiguration = {
export type StepperProvider = {
analyze: (
s: Extract,
- opts: { apis: StepperApis; opts?: StepperProviderOpts },
+ opts: { apis: StepperApis },
) => StepConfiguration;
prepare: (
s: Extract,
- opts: { apis: StepperApis; opts?: StepperProviderOpts },
+ opts: { apis: StepperApis },
) => StepConfiguration;
review: (
s: Extract,
- opts: { apis: StepperApis; opts?: StepperProviderOpts },
+ opts: { apis: StepperApis },
) => StepConfiguration;
finish: (
s: Extract,
- opts: { apis: StepperApis; opts?: StepperProviderOpts },
+ opts: { apis: StepperApis },
) => StepConfiguration;
};
-function defaultPreparePullRequest(
- apis: StepperApis,
- { title, body }: { title?: string; body?: string } = {},
-) {
- const appTitle = apis.configApi.getOptionalString('app.title') ?? 'Backstage';
- const appBaseUrl = apis.configApi.getString('app.baseUrl');
-
- return {
- title: title ?? 'Add catalog-info.yaml config file',
- body:
- 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, \
-the component will become available.\n\nFor more information, read an \
-[overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview).`,
- };
-}
-
/**
* The default stepper generation function.
*
@@ -169,13 +136,6 @@ export function defaultGenerateStepper(
return defaults.prepare(state, opts);
}
- const preparePullRequest =
- opts?.opts?.pullRequest?.preparePullRequest;
- const { title, body } = defaultPreparePullRequest(
- opts.apis,
- preparePullRequest ? preparePullRequest(opts.apis) : {},
- );
-
return {
stepLabel: Create Pull Request,
content: (
@@ -183,8 +143,6 @@ export function defaultGenerateStepper(
analyzeResult={state.analyzeResult}
onPrepare={state.onPrepare}
onGoBack={state.onGoBack}
- defaultTitle={title}
- defaultBody={body}
renderFormFields={({
values,
setValue,
@@ -299,14 +257,14 @@ export function defaultGenerateStepper(
}
export const defaultStepper: StepperProvider = {
- analyze: (state, { opts }) => ({
+ analyze: (state, { apis }) => ({
stepLabel: Select URL,
content: (
),
}),
diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx
index 9cc9532a3d..4a72a653ea 100644
--- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx
+++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx
@@ -14,15 +14,14 @@
* limitations under the License.
*/
+import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
+import { errorApiRef } from '@backstage/core-plugin-api';
import { act, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { AnalyzeResult, catalogImportApiRef } from '../../api/';
import { StepInitAnalyzeUrl } from './StepInitAnalyzeUrl';
-import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
-import { errorApiRef } from '@backstage/core-plugin-api';
-
describe('', () => {
const catalogImportApi: jest.Mocked = {
analyzeUrl: jest.fn(),
diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx
index 08b8177291..2212daf0c0 100644
--- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx
+++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
+import { errorApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { TextField } from '@material-ui/core';
import { act, render, screen } from '@testing-library/react';
@@ -25,12 +27,12 @@ import {
generateEntities,
StepPrepareCreatePullRequest,
} from './StepPrepareCreatePullRequest';
-import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
describe('', () => {
const catalogImportApi: jest.Mocked = {
analyzeUrl: jest.fn(),
submitPullRequest: jest.fn(),
+ preparePullRequest: jest.fn(),
};
const catalogApi: jest.Mocked = {
@@ -44,12 +46,16 @@ describe('', () => {
removeEntityByUid: jest.fn(),
};
+ const errorApi: jest.Mocked = {
+ error$: jest.fn(),
+ post: jest.fn(),
+ };
+
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
{children}
@@ -77,16 +83,19 @@ describe('', () => {
beforeEach(() => {
jest.resetAllMocks();
+
+ (catalogImportApi.preparePullRequest! as jest.Mock).mockResolvedValue({
+ title: 'My title',
+ body: 'My **body**',
+ });
});
it('renders without exploding', async () => {
catalogApi.getEntities.mockReturnValue(Promise.resolve({ items: [] }));
await act(async () => {
- const { getByText } = render(
+ const { findByText } = render(
{
@@ -105,8 +114,8 @@ describe('', () => {
},
);
- const title = getByText('My title');
- const description = getByText('body', { selector: 'strong' });
+ const title = await findByText('My title');
+ const description = await findByText('body', { selector: 'strong' });
expect(title).toBeInTheDocument();
expect(title).toBeVisible();
expect(description).toBeInTheDocument();
@@ -124,10 +133,8 @@ describe('', () => {
);
await act(async () => {
- await render(
+ render(
{
@@ -154,11 +161,9 @@ describe('', () => {
},
);
- await userEvent.type(await screen.getByLabelText('name'), '-changed');
- await userEvent.type(await screen.getByLabelText('owner'), '-changed');
- await userEvent.click(
- await screen.getByRole('button', { name: /Create PR/i }),
- );
+ userEvent.type(await screen.findByLabelText('name'), '-changed');
+ userEvent.type(await screen.findByLabelText('owner'), '-changed');
+ userEvent.click(screen.getByRole('button', { name: /Create PR/i }));
});
expect(catalogImportApi.submitPullRequest).toBeCalledTimes(1);
@@ -212,10 +217,8 @@ spec:
);
await act(async () => {
- await render(
+ render(
{
@@ -234,8 +237,8 @@ spec:
},
);
- await userEvent.click(
- await screen.getByRole('button', { name: /Create PR/i }),
+ userEvent.click(
+ await screen.findByRole('button', { name: /Create PR/i }),
);
});
@@ -261,10 +264,8 @@ spec:
);
await act(async () => {
- await render(
+ render(
void;
onGoBack?: () => void;
- defaultTitle: string;
- defaultBody: string;
-
renderFormFields: (
props: Pick<
UseFormReturn,
@@ -99,16 +96,30 @@ export const StepPrepareCreatePullRequest = ({
onPrepare,
onGoBack,
renderFormFields,
- defaultTitle,
- defaultBody,
}: Props) => {
const classes = useStyles();
const catalogApi = useApi(catalogApiRef);
- const catalogInfoApi = useApi(catalogImportApiRef);
+ const catalogImportApi = useApi(catalogImportApiRef);
+ const errorApi = useApi(errorApiRef);
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState();
+ const {
+ loading: prDefaultsLoading,
+ value: prDefaults,
+ error: prDefaultsError,
+ } = useAsync(
+ () => catalogImportApi.preparePullRequest!(),
+ [catalogImportApi.preparePullRequest],
+ );
+
+ useEffect(() => {
+ if (prDefaultsError) {
+ errorApi.post(prDefaultsError);
+ }
+ }, [prDefaultsError, errorApi]);
+
const { loading: groupsLoading, value: groups } = useAsync(async () => {
const groupEntities = await catalogApi.getEntities({
filter: { kind: 'group' },
@@ -124,7 +135,7 @@ export const StepPrepareCreatePullRequest = ({
setSubmitted(true);
try {
- const pr = await catalogInfoApi.submitPullRequest({
+ const pr = await catalogImportApi.submitPullRequest({
repositoryUrl: analyzeResult.url,
title: data.title,
body: data.body,
@@ -171,7 +182,7 @@ export const StepPrepareCreatePullRequest = ({
analyzeResult.generatedEntities,
analyzeResult.integrationType,
analyzeResult.url,
- catalogInfoApi,
+ catalogImportApi,
onPrepare,
],
);
@@ -184,79 +195,81 @@ export const StepPrepareCreatePullRequest = ({
a Pull Request that creates one.
-
- onSubmit={handleResult}
- defaultValues={{
- title: defaultTitle,
- body: defaultBody,
- owner:
- (analyzeResult.generatedEntities[0]?.spec?.owner as string) || '',
- componentName:
- analyzeResult.generatedEntities[0]?.metadata?.name || '',
- useCodeowners: false,
- }}
- render={({ values, formState, register, setValue }) => (
- <>
- {renderFormFields({
- values,
- formState,
- register,
- setValue,
- groups: groups ?? [],
- groupsLoading,
- })}
+ {!prDefaultsLoading && (
+
+ onSubmit={handleResult}
+ defaultValues={{
+ title: prDefaults?.title ?? '',
+ body: prDefaults?.body ?? '',
+ owner:
+ (analyzeResult.generatedEntities[0]?.spec?.owner as string) || '',
+ componentName:
+ analyzeResult.generatedEntities[0]?.metadata?.name || '',
+ useCodeowners: false,
+ }}
+ render={({ values, formState, register, setValue }) => (
+ <>
+ {renderFormFields({
+ values,
+ formState,
+ register,
+ setValue,
+ groups: groups ?? [],
+ groupsLoading,
+ })}
-
- Preview Pull Request
-
+
+ Preview Pull Request
+
-
+
-
- Preview Entities
-
+
+ Preview Entities
+
-
-
- {error && {error}}
-
-
- {onGoBack && (
-
- )}
-
- Create PR
-
-
- >
- )}
- />
+ repositoryUrl={analyzeResult.url}
+ classes={{
+ card: classes.previewCard,
+ cardContent: classes.previewCardContent,
+ }}
+ />
+
+ {error && {error}}
+
+
+ {onGoBack && (
+
+ )}
+
+ Create PR
+
+
+ >
+ )}
+ />
+ )}
>
);
};
diff --git a/plugins/catalog-import/src/components/index.ts b/plugins/catalog-import/src/components/index.ts
index 9b53200317..886c679d2d 100644
--- a/plugins/catalog-import/src/components/index.ts
+++ b/plugins/catalog-import/src/components/index.ts
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-export * from './ImportStepper';
+export * from './DefaultImportPage';
export * from './EntityListComponent';
+export * from './ImportInfoCard';
+export * from './ImportStepper';
export * from './StepInitAnalyzeUrl';
export * from './StepPrepareCreatePullRequest';
diff --git a/plugins/catalog-import/src/components/types.ts b/plugins/catalog-import/src/components/types.ts
new file mode 100644
index 0000000000..baa279ff21
--- /dev/null
+++ b/plugins/catalog-import/src/components/types.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 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 { CatalogImportApi } from '../api';
+
+export type StepperApis = {
+ catalogImportApi: CatalogImportApi;
+};
diff --git a/plugins/catalog-import/src/index.ts b/plugins/catalog-import/src/index.ts
index adee41bdbd..149c3e55c6 100644
--- a/plugins/catalog-import/src/index.ts
+++ b/plugins/catalog-import/src/index.ts
@@ -25,6 +25,5 @@ export {
catalogImportPlugin as plugin,
CatalogImportPage,
} from './plugin';
-export { Router } from './components/Router';
export * from './components';
export * from './api';
diff --git a/plugins/catalog-import/src/plugin.ts b/plugins/catalog-import/src/plugin.ts
index 3ab999ac07..d7d6370de3 100644
--- a/plugins/catalog-import/src/plugin.ts
+++ b/plugins/catalog-import/src/plugin.ts
@@ -14,10 +14,8 @@
* limitations under the License.
*/
-import { scmIntegrationsApiRef } from '@backstage/integration-react';
-import { catalogApiRef } from '@backstage/plugin-catalog-react';
-import { catalogImportApiRef, CatalogImportClient } from './api';
import {
+ configApiRef,
createApiFactory,
createPlugin,
createRoutableExtension,
@@ -26,6 +24,9 @@ import {
githubAuthApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
+import { scmIntegrationsApiRef } from '@backstage/integration-react';
+import { catalogApiRef } from '@backstage/plugin-catalog-react';
+import { catalogImportApiRef, CatalogImportClient } from './api';
export const rootRouteRef = createRouteRef({
path: '',
@@ -43,6 +44,7 @@ export const catalogImportPlugin = createPlugin({
identityApi: identityApiRef,
scmIntegrationsApi: scmIntegrationsApiRef,
catalogApi: catalogApiRef,
+ configApi: configApiRef,
},
factory: ({
discoveryApi,
@@ -50,6 +52,7 @@ export const catalogImportPlugin = createPlugin({
identityApi,
scmIntegrationsApi,
catalogApi,
+ configApi,
}) =>
new CatalogImportClient({
discoveryApi,
@@ -57,6 +60,7 @@ export const catalogImportPlugin = createPlugin({
scmIntegrationsApi,
identityApi,
catalogApi,
+ configApi,
}),
}),
],
@@ -67,7 +71,7 @@ export const catalogImportPlugin = createPlugin({
export const CatalogImportPage = catalogImportPlugin.provide(
createRoutableExtension({
- component: () => import('./components/Router').then(m => m.Router),
+ component: () => import('./components/ImportPage').then(m => m.ImportPage),
mountPoint: rootRouteRef,
}),
);