Merge pull request #6162 from SDA-SE/feat/catalog-import-customize
Add initial support for customizing the catalog import page
This commit is contained in:
@@ -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
|
||||
<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.
|
||||
|
||||
The export of the long term deprecated legacy `<Router>` is removed, migrate to
|
||||
`<CatalogImportPage>` instead.
|
||||
@@ -68,6 +68,11 @@ export interface CatalogImportApi {
|
||||
// (undocumented)
|
||||
analyzeUrl(url: string): Promise<AnalyzeResult>;
|
||||
// (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<AnalyzeResult>;
|
||||
// (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:
|
||||
|
||||
@@ -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: <ImportComponentPage />,
|
||||
element: <ImportPage />,
|
||||
})
|
||||
.addPage({
|
||||
title: 'Catalog Import 2',
|
||||
|
||||
@@ -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<CatalogImportApi>({
|
||||
id: 'plugin.catalog-import.service',
|
||||
@@ -42,6 +42,10 @@ export type AnalyzeResult =
|
||||
export interface CatalogImportApi {
|
||||
analyzeUrl(url: string): Promise<AnalyzeResult>;
|
||||
|
||||
preparePullRequest?(): Promise<{
|
||||
title: string;
|
||||
body: string;
|
||||
}>;
|
||||
submitPullRequest(options: {
|
||||
repositoryUrl: string;
|
||||
fileContent: string;
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<AnalyzeResult> {
|
||||
@@ -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,
|
||||
|
||||
+15
-20
@@ -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('<ImportComponentPage />', () => {
|
||||
describe('<DefaultImportPage />', () => {
|
||||
const identityApi = {
|
||||
getUserId: () => {
|
||||
return 'user';
|
||||
@@ -63,23 +61,20 @@ describe('<ImportComponentPage />', () => {
|
||||
identityApi,
|
||||
scmIntegrationsApi: {} as any,
|
||||
catalogApi: {} as any,
|
||||
configApi: {} as any,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders without exploding', async () => {
|
||||
await act(async () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<ImportComponentPage />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<DefaultImportPage />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(
|
||||
await getByText('Start tracking your component in Backstage'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
getByText('Start tracking your component in Backstage'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<Page themeId="home">
|
||||
<Header title="Register an existing component" />
|
||||
<Content>
|
||||
<ContentHeader title={`Start tracking your component in ${appTitle}`}>
|
||||
<SupportButton>
|
||||
Start tracking your component in {appTitle} 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}>
|
||||
<ImportInfoCard />
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={8} lg={6} xl={4}>
|
||||
<ImportStepper />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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 (
|
||||
<Page themeId="home">
|
||||
<Header title="Register an existing component" />
|
||||
<Content>
|
||||
<ContentHeader title={`Start tracking your component in ${appTitle}`}>
|
||||
<SupportButton>
|
||||
Start tracking your component in {appTitle} 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}>
|
||||
<InfoCard
|
||||
title="Register an existing component"
|
||||
deepLink={{
|
||||
title: 'Learn more about the Software Catalog',
|
||||
link: 'https://backstage.io/docs/features/software-catalog/software-catalog-overview',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" paragraph>
|
||||
Enter the URL to your source code repository to add it to{' '}
|
||||
{appTitle}.
|
||||
</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>
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
The wizard analyzes the file, previews the entities, and adds
|
||||
them to the {appTitle} catalog.
|
||||
</Typography>
|
||||
{hasGithubIntegration && (
|
||||
<>
|
||||
<Typography variant="h6">
|
||||
Link to a repository{' '}
|
||||
<Chip label="GitHub only" variant="outlined" size="small" />
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
color="textSecondary"
|
||||
paragraph
|
||||
>
|
||||
Example: <code>https://github.com/backstage/backstage</code>
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
The wizard discovers all <code>catalog-info.yaml</code>{' '}
|
||||
files in the repository, previews the entities, and adds
|
||||
them to the {appTitle} catalog.
|
||||
</Typography>
|
||||
{!opts?.pullRequest?.disable && (
|
||||
<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 the {appTitle}{' '}
|
||||
catalog to load all entities as soon as the Pull Request
|
||||
is merged.
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={8} lg={6} xl={4}>
|
||||
<ImportStepper opts={opts} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -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('<ImportInfoCard />', () => {
|
||||
let apis: ApiRegistry;
|
||||
let catalogImportApi: jest.Mocked<CatalogImportApi>;
|
||||
|
||||
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(
|
||||
<ApiProvider apis={apis}>
|
||||
<ImportInfoCard />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<ApiProvider apis={apis}>
|
||||
<ImportInfoCard />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<ApiProvider apis={apis}>
|
||||
<ImportInfoCard />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(
|
||||
getByText(/the wizard will prepare a Pull Request/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<InfoCard
|
||||
title="Register an existing component"
|
||||
deepLink={{
|
||||
title: 'Learn more about the Software Catalog',
|
||||
link: 'https://backstage.io/docs/features/software-catalog/software-catalog-overview',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" paragraph>
|
||||
Enter the URL to your source code repository to add it to {appTitle}.
|
||||
</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>
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
The wizard analyzes the file, previews the entities, and adds them to
|
||||
the {appTitle} catalog.
|
||||
</Typography>
|
||||
{hasGithubIntegration && (
|
||||
<>
|
||||
<Typography variant="h6">
|
||||
Link to a repository{' '}
|
||||
<Chip label="GitHub only" variant="outlined" size="small" />
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" color="textSecondary" paragraph>
|
||||
Example: <code>https://github.com/backstage/backstage</code>
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
The wizard discovers all <code>catalog-info.yaml</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
|
||||
the {appTitle} catalog to load all entities as soon as the Pull
|
||||
Request is merged.
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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('<ImportPage />', () => {
|
||||
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(
|
||||
<ApiProvider apis={apis}>
|
||||
<ImportPage />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(
|
||||
getByText('Start tracking your component in Backstage'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with custom children', async () => {
|
||||
(useOutlet as jest.Mock).mockReturnValue(<div>Hello World</div>);
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<ImportPage />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(getByText('Hello World')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+7
-8
@@ -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) => (
|
||||
<Routes>
|
||||
<Route element={<ImportComponentPage {...opts} />} />
|
||||
</Routes>
|
||||
);
|
||||
export const ImportPage = () => {
|
||||
const outlet = useOutlet();
|
||||
|
||||
return outlet || <DefaultImportPage />;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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<ImportState, { activeState: 'analyze' }>,
|
||||
{ apis: { configApi }, opts },
|
||||
{ apis: { catalogImportApi } },
|
||||
),
|
||||
)}
|
||||
{render(
|
||||
states.prepare(
|
||||
state as Extract<ImportState, { activeState: 'prepare' }>,
|
||||
{ apis: { configApi }, opts },
|
||||
{ apis: { catalogImportApi } },
|
||||
),
|
||||
)}
|
||||
{render(
|
||||
states.review(
|
||||
state as Extract<ImportState, { activeState: 'review' }>,
|
||||
{ apis: { configApi }, opts },
|
||||
{ apis: { catalogImportApi } },
|
||||
),
|
||||
)}
|
||||
{render(
|
||||
states.finish(
|
||||
state as Extract<ImportState, { activeState: 'finish' }>,
|
||||
{ apis: { configApi }, opts },
|
||||
{ apis: { catalogImportApi } },
|
||||
),
|
||||
)}
|
||||
</Stepper>
|
||||
|
||||
@@ -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<ImportState, { activeState: 'analyze' }>,
|
||||
opts: { apis: StepperApis; opts?: StepperProviderOpts },
|
||||
opts: { apis: StepperApis },
|
||||
) => StepConfiguration;
|
||||
prepare: (
|
||||
s: Extract<ImportState, { activeState: 'prepare' }>,
|
||||
opts: { apis: StepperApis; opts?: StepperProviderOpts },
|
||||
opts: { apis: StepperApis },
|
||||
) => StepConfiguration;
|
||||
review: (
|
||||
s: Extract<ImportState, { activeState: 'review' }>,
|
||||
opts: { apis: StepperApis; opts?: StepperProviderOpts },
|
||||
opts: { apis: StepperApis },
|
||||
) => StepConfiguration;
|
||||
finish: (
|
||||
s: Extract<ImportState, { activeState: 'finish' }>,
|
||||
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: <StepLabel>Create Pull Request</StepLabel>,
|
||||
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: <StepLabel>Select URL</StepLabel>,
|
||||
content: (
|
||||
<StepInitAnalyzeUrl
|
||||
key="analyze"
|
||||
analysisUrl={state.analysisUrl}
|
||||
onAnalysis={state.onAnalysis}
|
||||
disablePullRequest={opts?.pullRequest?.disable}
|
||||
disablePullRequest={!apis.catalogImportApi.preparePullRequest}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
|
||||
+2
-3
@@ -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('<StepInitAnalyzeUrl />', () => {
|
||||
const catalogImportApi: jest.Mocked<typeof catalogImportApiRef.T> = {
|
||||
analyzeUrl: jest.fn(),
|
||||
|
||||
+27
-26
@@ -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('<StepPrepareCreatePullRequest />', () => {
|
||||
const catalogImportApi: jest.Mocked<typeof catalogImportApiRef.T> = {
|
||||
analyzeUrl: jest.fn(),
|
||||
submitPullRequest: jest.fn(),
|
||||
preparePullRequest: jest.fn(),
|
||||
};
|
||||
|
||||
const catalogApi: jest.Mocked<typeof catalogApiRef.T> = {
|
||||
@@ -44,12 +46,16 @@ describe('<StepPrepareCreatePullRequest />', () => {
|
||||
removeEntityByUid: jest.fn(),
|
||||
};
|
||||
|
||||
const errorApi: jest.Mocked<typeof errorApiRef.T> = {
|
||||
error$: jest.fn(),
|
||||
post: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.with(catalogImportApiRef, catalogImportApi).with(
|
||||
catalogApiRef,
|
||||
catalogApi,
|
||||
)}
|
||||
apis={ApiRegistry.with(catalogImportApiRef, catalogImportApi)
|
||||
.with(catalogApiRef, catalogApi)
|
||||
.with(errorApiRef, errorApi)}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
@@ -77,16 +83,19 @@ describe('<StepPrepareCreatePullRequest />', () => {
|
||||
|
||||
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(
|
||||
<StepPrepareCreatePullRequest
|
||||
defaultTitle="My title"
|
||||
defaultBody="My **body**"
|
||||
analyzeResult={analyzeResult}
|
||||
onPrepare={onPrepareFn}
|
||||
renderFormFields={({ register }) => {
|
||||
@@ -105,8 +114,8 @@ describe('<StepPrepareCreatePullRequest />', () => {
|
||||
},
|
||||
);
|
||||
|
||||
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('<StepPrepareCreatePullRequest />', () => {
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await render(
|
||||
render(
|
||||
<StepPrepareCreatePullRequest
|
||||
defaultTitle="My title"
|
||||
defaultBody="My **body**"
|
||||
analyzeResult={analyzeResult}
|
||||
onPrepare={onPrepareFn}
|
||||
renderFormFields={({ register }) => {
|
||||
@@ -154,11 +161,9 @@ describe('<StepPrepareCreatePullRequest />', () => {
|
||||
},
|
||||
);
|
||||
|
||||
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(
|
||||
<StepPrepareCreatePullRequest
|
||||
defaultTitle="My title"
|
||||
defaultBody="My **body**"
|
||||
analyzeResult={analyzeResult}
|
||||
onPrepare={onPrepareFn}
|
||||
renderFormFields={({ register }) => {
|
||||
@@ -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(
|
||||
<StepPrepareCreatePullRequest
|
||||
defaultTitle="My title"
|
||||
defaultBody="My **body**"
|
||||
analyzeResult={analyzeResult}
|
||||
onPrepare={onPrepareFn}
|
||||
renderFormFields={renderFormFieldsFn}
|
||||
|
||||
+91
-78
@@ -15,14 +15,14 @@
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
catalogApiRef,
|
||||
formatEntityRefTitle,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Box, FormHelperText, Grid, Typography } from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { UnpackNestedValue, UseFormReturn } from 'react-hook-form';
|
||||
import { useAsync } from 'react-use';
|
||||
import YAML from 'yaml';
|
||||
@@ -59,9 +59,6 @@ type Props = {
|
||||
) => void;
|
||||
onGoBack?: () => void;
|
||||
|
||||
defaultTitle: string;
|
||||
defaultBody: string;
|
||||
|
||||
renderFormFields: (
|
||||
props: Pick<
|
||||
UseFormReturn<FormData>,
|
||||
@@ -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<string>();
|
||||
|
||||
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.
|
||||
</Typography>
|
||||
|
||||
<PreparePullRequestForm<FormData>
|
||||
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 && (
|
||||
<PreparePullRequestForm<FormData>
|
||||
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,
|
||||
})}
|
||||
|
||||
<Box marginTop={2}>
|
||||
<Typography variant="h6">Preview Pull Request</Typography>
|
||||
</Box>
|
||||
<Box marginTop={2}>
|
||||
<Typography variant="h6">Preview Pull Request</Typography>
|
||||
</Box>
|
||||
|
||||
<PreviewPullRequestComponent
|
||||
title={values.title}
|
||||
description={values.body}
|
||||
classes={{
|
||||
card: classes.previewCard,
|
||||
cardContent: classes.previewCardContent,
|
||||
}}
|
||||
/>
|
||||
<PreviewPullRequestComponent
|
||||
title={values.title}
|
||||
description={values.body}
|
||||
classes={{
|
||||
card: classes.previewCard,
|
||||
cardContent: classes.previewCardContent,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box marginTop={2} marginBottom={1}>
|
||||
<Typography variant="h6">Preview Entities</Typography>
|
||||
</Box>
|
||||
<Box marginTop={2} marginBottom={1}>
|
||||
<Typography variant="h6">Preview Entities</Typography>
|
||||
</Box>
|
||||
|
||||
<PreviewCatalogInfoComponent
|
||||
entities={generateEntities(
|
||||
analyzeResult.generatedEntities,
|
||||
values.componentName,
|
||||
values.owner,
|
||||
)}
|
||||
repositoryUrl={analyzeResult.url}
|
||||
classes={{
|
||||
card: classes.previewCard,
|
||||
cardContent: classes.previewCardContent,
|
||||
}}
|
||||
/>
|
||||
|
||||
{error && <FormHelperText error>{error}</FormHelperText>}
|
||||
|
||||
<Grid container spacing={0}>
|
||||
{onGoBack && (
|
||||
<BackButton onClick={onGoBack} disabled={submitted} />
|
||||
)}
|
||||
<NextButton
|
||||
type="submit"
|
||||
disabled={Boolean(
|
||||
formState.errors.title ||
|
||||
formState.errors.body ||
|
||||
formState.errors.owner,
|
||||
<PreviewCatalogInfoComponent
|
||||
entities={generateEntities(
|
||||
analyzeResult.generatedEntities,
|
||||
values.componentName,
|
||||
values.owner,
|
||||
)}
|
||||
loading={submitted}
|
||||
>
|
||||
Create PR
|
||||
</NextButton>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
repositoryUrl={analyzeResult.url}
|
||||
classes={{
|
||||
card: classes.previewCard,
|
||||
cardContent: classes.previewCardContent,
|
||||
}}
|
||||
/>
|
||||
|
||||
{error && <FormHelperText error>{error}</FormHelperText>}
|
||||
|
||||
<Grid container spacing={0}>
|
||||
{onGoBack && (
|
||||
<BackButton onClick={onGoBack} disabled={submitted} />
|
||||
)}
|
||||
<NextButton
|
||||
type="submit"
|
||||
disabled={Boolean(
|
||||
formState.errors.title ||
|
||||
formState.errors.body ||
|
||||
formState.errors.owner,
|
||||
)}
|
||||
loading={submitted}
|
||||
>
|
||||
Create PR
|
||||
</NextButton>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -25,6 +25,5 @@ export {
|
||||
catalogImportPlugin as plugin,
|
||||
CatalogImportPage,
|
||||
} from './plugin';
|
||||
export { Router } from './components/Router';
|
||||
export * from './components';
|
||||
export * from './api';
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user