Make preparePullRequest async

Signed-off-by: Oliver Sand <oliver.sand@sda-se.com>
This commit is contained in:
Oliver Sand
2021-09-16 16:05:11 +02:00
parent 66a9e17035
commit 18571288a8
7 changed files with 139 additions and 117 deletions
@@ -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,10 +42,10 @@ export type AnalyzeResult =
export interface CatalogImportApi {
analyzeUrl(url: string): Promise<AnalyzeResult>;
preparePullRequest?(): {
preparePullRequest?(): Promise<{
title: string;
body: string;
};
}>;
submitPullRequest(options: {
repositoryUrl: string;
fileContent: string;
@@ -115,7 +115,11 @@ describe('CatalogImportClient', () => {
scmIntegrationsApi,
identityApi,
catalogApi,
configApi: new ConfigReader({}),
configApi: new ConfigReader({
app: {
baseUrl: 'https://demo.backstage.io/',
},
}),
});
});
@@ -444,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),
});
});
});
});
@@ -118,10 +118,10 @@ export class CatalogImportClient implements CatalogImportApi {
};
}
preparePullRequest(): {
async preparePullRequest(): Promise<{
title: string;
body: string;
} {
}> {
const appTitle =
this.configApi.getOptionalString('app.title') ?? 'Backstage';
const appBaseUrl = this.configApi.getString('app.baseUrl');
@@ -66,7 +66,7 @@ describe('<ImportInfoCard />', () => {
});
it('renders section on GitHub discovery if supported', async () => {
catalogImportApi.preparePullRequest = () => ({ title: '', body: '' });
catalogImportApi.preparePullRequest = async () => ({ title: '', body: '' });
await act(async () => {
const { getByText } = render(
@@ -82,7 +82,7 @@ describe('<ImportInfoCard />', () => {
});
it('renders section on pull requests if supported', async () => {
catalogImportApi.preparePullRequest = () => ({ title: '', body: '' });
catalogImportApi.preparePullRequest = async () => ({ title: '', body: '' });
await act(async () => {
const { getByText } = render(
@@ -136,9 +136,6 @@ export function defaultGenerateStepper(
return defaults.prepare(state, opts);
}
const { title, body } =
opts.apis.catalogImportApi.preparePullRequest!();
return {
stepLabel: <StepLabel>Create Pull Request</StepLabel>,
content: (
@@ -146,8 +143,6 @@ export function defaultGenerateStepper(
analyzeResult={state.analyzeResult}
onPrepare={state.onPrepare}
onGoBack={state.onGoBack}
defaultTitle={title}
defaultBody={body}
renderFormFields={({
values,
setValue,
@@ -14,23 +14,25 @@
* limitations under the License.
*/
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { TextField } from '@material-ui/core';
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { errorApiRef } from '../../../../../packages/core-plugin-api/src';
import { AnalyzeResult, catalogImportApiRef } from '../../api';
import { asInputRef } from '../helpers';
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}
@@ -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>
</>
)}
/>
)}
</>
);
};