Add initial support for customizing the catalog import page

Signed-off-by: Oliver Sand <oliver.sand@sda-se.com>
This commit is contained in:
Oliver Sand
2021-06-22 13:41:33 +02:00
parent 639a7ed79c
commit 690657799e
19 changed files with 483 additions and 142 deletions
+35
View File
@@ -0,0 +1,35 @@
---
'@backstage/plugin-catalog-import': patch
---
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>
```
@@ -0,0 +1,84 @@
/*
* 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,
configApiRef,
ConfigReader,
} from '@backstage/core';
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 { DefaultImportComponentPage } from './DefaultImportComponentPage';
describe('<DefaultImportComponentPage />', () => {
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,
}),
);
});
it('renders without exploding', async () => {
await act(async () => {
const { getByText } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
<DefaultImportComponentPage />
</ApiProvider>,
),
);
expect(
getByText('Start tracking your component in Backstage'),
).toBeInTheDocument();
});
});
});
@@ -0,0 +1,58 @@
/*
* 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 {
configApiRef,
Content,
ContentHeader,
Header,
Page,
SupportButton,
useApi,
} from '@backstage/core';
import { Grid } from '@material-ui/core';
import React from 'react';
import { ImportInfoCard } from '../ImportInfoCard';
import { ImportStepper } from '../ImportStepper';
export const DefaultImportComponentPage = () => {
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 { DefaultImportComponentPage } from './DefaultImportComponentPage';
@@ -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>
);
};
@@ -19,7 +19,7 @@ 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 { catalogImportApiRef, CatalogImportClient } from '../../api';
import { ImportComponentPage } from './ImportComponentPage';
import {
@@ -78,7 +78,7 @@ describe('<ImportComponentPage />', () => {
);
expect(
await getByText('Start tracking your component in Backstage'),
getByText('Start tracking your component in Backstage'),
).toBeInTheDocument();
});
});
@@ -0,0 +1,31 @@
/*
* 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 React from 'react';
import { useOutlet } from 'react-router';
import { DefaultImportComponentPage } from '../DefaultImportComponentPage';
import { ImportOptionsContext } from '../ImportOptionsContext';
import { ImportOptions } from '../types';
export const ImportComponentPage = (opts: ImportOptions) => {
const outlet = useOutlet();
return (
<ImportOptionsContext.Provider value={opts}>
{outlet || <DefaultImportComponentPage />}
</ImportOptionsContext.Provider>
);
};
@@ -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 { ImportComponentPage } from './ImportComponentPage';
@@ -0,0 +1,51 @@
/*
* 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,
configApiRef,
ConfigReader,
} from '@backstage/core';
import { wrapInTestApp } from '@backstage/test-utils';
import { act, render } from '@testing-library/react';
import React from 'react';
import { ImportInfoCard } from './ImportInfoCard';
describe('<ImportInfoCard />', () => {
let apis: ApiRegistry;
beforeEach(() => {
apis = ApiRegistry.with(
configApiRef,
new ConfigReader({ integrations: {} }),
);
});
it('renders without exploding', async () => {
await act(async () => {
const { getByText } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
<ImportInfoCard />
</ApiProvider>,
),
);
expect(getByText('Register an existing component')).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 { ConfigApi, configApiRef, InfoCard, useApi } from '@backstage/core';
import { Typography } from '@material-ui/core';
import React from 'react';
import { useImportOptions } from '../ImportOptionsContext';
export const ImportInfoCard = () => {
const configApi = useApi(configApiRef);
const appTitle = configApi.getOptional('app.title') || 'Backstage';
const opts = useImportOptions();
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>
{!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>
);
};
@@ -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,24 @@
/*
* 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 { createContext, useContext } from 'react';
import { ImportOptions } from '../types';
export const ImportOptionsContext = createContext<ImportOptions>({});
export const useImportOptions = (): ImportOptions => {
return useContext(ImportOptionsContext);
};
@@ -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 { ImportOptionsContext, useImportOptions } from './ImportOptionsContext';
@@ -17,13 +17,14 @@
import { Step, StepContent, Stepper } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import React, { useMemo } from 'react';
import { useImportOptions } from '../ImportOptionsContext';
import { ImportOptions } from '../types';
import { ImportFlows, ImportState, useImportState } from '../useImportState';
import {
defaultGenerateStepper,
defaultStepper,
StepConfiguration,
StepperProvider,
StepperProviderOpts,
} from './defaults';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
@@ -42,18 +43,19 @@ type Props = {
defaults: StepperProvider,
) => StepperProvider;
variant?: InfoCardVariants;
opts?: StepperProviderOpts;
/// @deprecated Pass import options via ImportOptionsContext instead.
opts?: ImportOptions;
};
export const ImportStepper = ({
initialUrl,
generateStepper = defaultGenerateStepper,
variant,
opts,
}: Props) => {
const configApi = useApi(configApiRef);
const classes = useStyles();
const state = useImportState({ initialUrl });
const opts = useImportOptions();
const states = useMemo<StepperProvider>(
() => generateStepper(state.activeFlow, defaultStepper),
@@ -35,22 +35,9 @@ import {
} from '../StepPrepareCreatePullRequest';
import { StepPrepareSelectLocations } from '../StepPrepareSelectLocations';
import { StepReviewLocation } from '../StepReviewLocation';
import { ImportOptions, 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,19 +46,19 @@ export type StepConfiguration = {
export type StepperProvider = {
analyze: (
s: Extract<ImportState, { activeState: 'analyze' }>,
opts: { apis: StepperApis; opts?: StepperProviderOpts },
opts: { apis: StepperApis; opts?: ImportOptions },
) => StepConfiguration;
prepare: (
s: Extract<ImportState, { activeState: 'prepare' }>,
opts: { apis: StepperApis; opts?: StepperProviderOpts },
opts: { apis: StepperApis; opts?: ImportOptions },
) => StepConfiguration;
review: (
s: Extract<ImportState, { activeState: 'review' }>,
opts: { apis: StepperApis; opts?: StepperProviderOpts },
opts: { apis: StepperApis; opts?: ImportOptions },
) => StepConfiguration;
finish: (
s: Extract<ImportState, { activeState: 'finish' }>,
opts: { apis: StepperApis; opts?: StepperProviderOpts },
opts: { apis: StepperApis; opts?: ImportOptions },
) => StepConfiguration;
};
@@ -17,9 +17,10 @@
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { ImportComponentPage } from './ImportComponentPage';
import { StepperProviderOpts } from './ImportStepper/defaults';
import { ImportOptions } from './types';
export const Router = (opts: StepperProviderOpts) => (
/// @deprecated, use ImportComponentPage instead.
export const Router = (opts: ImportOptions) => (
<Routes>
<Route element={<ImportComponentPage {...opts} />} />
</Routes>
@@ -14,7 +14,10 @@
* limitations under the License.
*/
export * from './ImportStepper';
export * from './DefaultImportComponentPage';
export * from './EntityListComponent';
export * from './ImportInfoCard';
export * from './ImportOptionsContext';
export * from './ImportStepper';
export * from './StepInitAnalyzeUrl';
export * from './StepPrepareCreatePullRequest';
@@ -0,0 +1,30 @@
/*
* 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 { ConfigApi } from '@backstage/core-plugin-api';
export type ImportOptions = {
pullRequest?: {
disable?: boolean;
preparePullRequest?: (
apis: StepperApis,
) => { title?: string; body?: string };
};
};
export type StepperApis = {
configApi: ConfigApi;
};
+4 -1
View File
@@ -67,7 +67,10 @@ export const catalogImportPlugin = createPlugin({
export const CatalogImportPage = catalogImportPlugin.provide(
createRoutableExtension({
component: () => import('./components/Router').then(m => m.Router),
component: () =>
import('./components/ImportComponentPage').then(
m => m.ImportComponentPage,
),
mountPoint: rootRouteRef,
}),
);