Change ragister-component to catalog-import
This commit is contained in:
@@ -68,7 +68,10 @@ const catalogRouteRef = createRouteRef({
|
||||
const AppRoutes = () => (
|
||||
<Routes>
|
||||
<Navigate key="/" to="/catalog" />
|
||||
<Route path="/catalog-import/*" element={<ImportComponentRouter />} />
|
||||
<Route
|
||||
path="/catalog-import"
|
||||
element={<ImportComponentRouter catalogRouteRef={catalogRouteRef} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${catalogRouteRef.path}/*`}
|
||||
element={<CatalogRouter EntityPage={EntityPage} />}
|
||||
|
||||
@@ -38,4 +38,5 @@ export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild';
|
||||
export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
|
||||
export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights';
|
||||
export { plugin as CatalogImport } from '@backstage/plugin-catalog-import';
|
||||
export { plugin as BulkCatalogImport } from '@roadiehq/backstage-plugin-bulk-catalog-import';
|
||||
export { plugin as UserSettings } from '@backstage/plugin-user-settings';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Logger } from 'winston';
|
||||
import GitUriParser from 'git-url-parse';
|
||||
import parseGitUri from 'git-url-parse';
|
||||
import {
|
||||
AnalyzeLocationRequest,
|
||||
AnalyzeLocationResponse,
|
||||
@@ -31,7 +31,7 @@ export class LocationAnalyzerClient implements LocationAnalyzer {
|
||||
async generateConfig(
|
||||
request: AnalyzeLocationRequest,
|
||||
): Promise<AnalyzeLocationResponse> {
|
||||
const { owner, name, source } = GitUriParser(request.location.target);
|
||||
const { owner, name, source } = parseGitUri(request.location.target);
|
||||
const entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-hook-form": "^6.6.0",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^15.3.3",
|
||||
"yaml": "^1.10.0"
|
||||
|
||||
@@ -15,57 +15,140 @@
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Button, CircularProgress, Grid, Tooltip } from '@material-ui/core';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import {
|
||||
Button,
|
||||
CircularProgress,
|
||||
Grid,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
Typography,
|
||||
Divider,
|
||||
} from '@material-ui/core';
|
||||
import { useGithubRepos } from '../util/useGithubRepos';
|
||||
import { ConfigSpec } from './ImportComponentPage';
|
||||
import { errorApiRef, useApi } from '@backstage/core';
|
||||
import {
|
||||
errorApiRef,
|
||||
RouteRef,
|
||||
StructuredMetadataTable,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import parseGitUri from 'git-url-parse';
|
||||
import { PartialEntity } from '../util/types';
|
||||
import { generatePath, resolvePath } from 'react-router';
|
||||
import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
const getEntityCatalogPath = ({
|
||||
entity,
|
||||
catalogRouteRef,
|
||||
}: {
|
||||
entity: PartialEntity;
|
||||
catalogRouteRef: RouteRef;
|
||||
}) => {
|
||||
const relativeEntityPathInsideCatalog = generatePath(
|
||||
entityRoute.path,
|
||||
entityRouteParams(entity as Entity),
|
||||
);
|
||||
|
||||
const resolvedAbsolutePath = resolvePath(
|
||||
relativeEntityPathInsideCatalog,
|
||||
catalogRouteRef.path,
|
||||
)?.pathname;
|
||||
|
||||
return resolvedAbsolutePath;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
nextStep: () => void;
|
||||
configFile: ConfigSpec;
|
||||
savePRLink: (PRLink: string) => void;
|
||||
catalogRouteRef: RouteRef;
|
||||
};
|
||||
|
||||
const ComponentConfigDisplay = ({
|
||||
nextStep,
|
||||
configFile,
|
||||
savePRLink,
|
||||
catalogRouteRef,
|
||||
}: Props) => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const { submitPrToRepo } = useGithubRepos();
|
||||
const { submitPrToRepo, addLocation } = useGithubRepos();
|
||||
const onNext = useCallback(async () => {
|
||||
try {
|
||||
setSubmitting(true);
|
||||
const result = await submitPrToRepo(configFile);
|
||||
savePRLink(result.link);
|
||||
setSubmitting(false);
|
||||
nextStep();
|
||||
if (!parseGitUri(configFile.location).filepathtype) {
|
||||
const result = await submitPrToRepo(configFile);
|
||||
savePRLink(result.link);
|
||||
setSubmitting(false);
|
||||
nextStep();
|
||||
} else {
|
||||
addLocation(configFile.location);
|
||||
setSubmitting(false);
|
||||
nextStep();
|
||||
}
|
||||
} catch (e) {
|
||||
setSubmitting(false);
|
||||
errorApi.post(e);
|
||||
}
|
||||
}, [submitPrToRepo, configFile, nextStep, savePRLink, errorApi]);
|
||||
}, [submitPrToRepo, configFile, nextStep, savePRLink, errorApi, addLocation]);
|
||||
|
||||
return (
|
||||
<Grid container direction="column" spacing={1}>
|
||||
{!parseGitUri(configFile.location).filepathtype ? (
|
||||
<Typography>
|
||||
Following config object will be submitted in a pull request to the
|
||||
repository{' '}
|
||||
<Link href={configFile.location}>{configFile.location}</Link> and
|
||||
added as a new location to the backend
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography>
|
||||
Following config object will be added as a new location to the backend{' '}
|
||||
<Link href={configFile.location}>{configFile.location}</Link>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Grid item>
|
||||
<Alert severity="success">
|
||||
config file has been generated. You can optionally edit it before
|
||||
submitting Pull Request
|
||||
</Alert>
|
||||
{!parseGitUri(configFile.location).filepathtype ? (
|
||||
<pre>{JSON.stringify(configFile.config, null, 2)}</pre>
|
||||
) : (
|
||||
<List>
|
||||
{configFile.config.map((entity: any, index: number) => {
|
||||
const entityPath = getEntityCatalogPath({
|
||||
entity,
|
||||
catalogRouteRef,
|
||||
});
|
||||
return (
|
||||
<React.Fragment
|
||||
key={`${entity.metadata.namespace}-${entity.metadata.name}`}
|
||||
>
|
||||
<ListItem>
|
||||
<StructuredMetadataTable
|
||||
dense
|
||||
metadata={{
|
||||
name: entity.metadata.name,
|
||||
type: entity.spec.type,
|
||||
link: (
|
||||
<Link component={RouterLink} to={entityPath}>
|
||||
{entityPath}
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{index < configFile.config.length - 1 && (
|
||||
<Divider component="li" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item container justify="flex-end" spacing={1}>
|
||||
<Grid item>
|
||||
<Tooltip title="Not available yet">
|
||||
<span>
|
||||
<Button variant="contained" disabled>
|
||||
Show config objects
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item container spacing={1}>
|
||||
{submitting ? (
|
||||
<Grid item>
|
||||
<CircularProgress size="2rem" />
|
||||
|
||||
@@ -26,9 +26,11 @@ import { makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useMountedState } from 'react-use';
|
||||
import parseGitUri from 'git-url-parse';
|
||||
import { ComponentIdValidators } from '../util/validate';
|
||||
import { useGithubRepos } from '../util/useGithubRepos';
|
||||
import { ConfigSpec } from './ImportComponentPage';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
form: {
|
||||
@@ -53,6 +55,7 @@ export const RegisterComponentForm = ({ nextStep, saveConfig }: Props) => {
|
||||
const classes = useStyles();
|
||||
const hasErrors = !!errors.componentLocation;
|
||||
const dirty = formState?.isDirty;
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const isMounted = useMountedState();
|
||||
const errorApi = useApi(errorApiRef);
|
||||
@@ -62,12 +65,22 @@ export const RegisterComponentForm = ({ nextStep, saveConfig }: Props) => {
|
||||
const { componentLocation: target } = formData;
|
||||
try {
|
||||
if (!isMounted()) return;
|
||||
const type = !parseGitUri(target).filepathtype ? 'repo' : 'file';
|
||||
|
||||
const config = await generateEntityDefinitions(target);
|
||||
saveConfig({
|
||||
repo: target,
|
||||
config,
|
||||
});
|
||||
if (type === 'repo') {
|
||||
saveConfig({
|
||||
type,
|
||||
location: target,
|
||||
config: await generateEntityDefinitions(target),
|
||||
});
|
||||
} else {
|
||||
const data = await catalogApi.addLocation({ target });
|
||||
saveConfig({
|
||||
type,
|
||||
location: data.location.target,
|
||||
config: data.entities,
|
||||
});
|
||||
}
|
||||
nextStep();
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
@@ -111,7 +124,7 @@ export const RegisterComponentForm = ({ nextStep, saveConfig }: Props) => {
|
||||
disabled={!dirty || hasErrors}
|
||||
className={classes.submit}
|
||||
>
|
||||
Submit
|
||||
Next
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Header,
|
||||
SupportButton,
|
||||
ContentHeader,
|
||||
RouteRef,
|
||||
} from '@backstage/core';
|
||||
import { RegisterComponentForm } from './ImportComponentForm';
|
||||
import ImportStepper from './ImportStepper';
|
||||
@@ -31,29 +32,35 @@ import { ImportFinished } from './ImportFinished';
|
||||
import { PartialEntity } from '../util/types';
|
||||
|
||||
export type ConfigSpec = {
|
||||
repo: string;
|
||||
type: 'repo' | 'file';
|
||||
location: string;
|
||||
config: PartialEntity[];
|
||||
};
|
||||
|
||||
export const ImportComponentPage = () => {
|
||||
export const ImportComponentPage = ({
|
||||
catalogRouteRef,
|
||||
}: {
|
||||
catalogRouteRef: RouteRef;
|
||||
}) => {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [configFile, setConfigFile] = useState<ConfigSpec>({
|
||||
repo: '',
|
||||
type: 'repo',
|
||||
location: '',
|
||||
config: [],
|
||||
});
|
||||
const [PRLink, setPRLink] = useState<string>('');
|
||||
const [endLink, setEndLink] = useState<string>('');
|
||||
const nextStep = (options?: { reset: boolean }) => {
|
||||
setActiveStep(step => (options?.reset ? 0 : step + 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header title="Import repo" />
|
||||
<Header title="Register an existing component" />
|
||||
<Content>
|
||||
<ContentHeader title="Import your repository to Backstage">
|
||||
<ContentHeader title="Start tracking your component on backstage">
|
||||
<SupportButton>
|
||||
Generate a component definition file and automatically submit it as
|
||||
a pull request. TODO: Add more information about what this is.
|
||||
Start tracking your component in Backstage. TODO: Add more
|
||||
information about what this is.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
@@ -62,7 +69,7 @@ export const ImportComponentPage = () => {
|
||||
<ImportStepper
|
||||
steps={[
|
||||
{
|
||||
step: 'Select GitHub repo and generate config files for it',
|
||||
step: 'Insert GitHub repo URL or Entity File URL',
|
||||
content: (
|
||||
<RegisterComponentForm
|
||||
nextStep={nextStep}
|
||||
@@ -71,19 +78,24 @@ export const ImportComponentPage = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
step: 'Review generated component config files',
|
||||
step: 'Review',
|
||||
content: (
|
||||
<ComponentConfigDisplay
|
||||
nextStep={nextStep}
|
||||
configFile={configFile}
|
||||
savePRLink={setPRLink}
|
||||
savePRLink={setEndLink}
|
||||
catalogRouteRef={catalogRouteRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
step: 'Finish',
|
||||
content: (
|
||||
<ImportFinished nextStep={nextStep} PRLink={PRLink} />
|
||||
<ImportFinished
|
||||
nextStep={nextStep}
|
||||
PRLink={endLink}
|
||||
type={configFile.type}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -13,52 +13,44 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import {
|
||||
Grid,
|
||||
Link,
|
||||
Typography,
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
} from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
heading: {
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
},
|
||||
}),
|
||||
);
|
||||
import React from 'react';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { Button, Grid, Link } from '@material-ui/core';
|
||||
|
||||
type Props = {
|
||||
type: 'repo' | 'file';
|
||||
nextStep: (options?: { reset: boolean }) => void;
|
||||
PRLink: string;
|
||||
};
|
||||
|
||||
export const ImportFinished = ({ nextStep, PRLink }: Props) => {
|
||||
const classes = useStyles();
|
||||
export const ImportFinished = ({ nextStep, PRLink, type }: Props) => {
|
||||
return (
|
||||
<Grid container direction="column" spacing={1}>
|
||||
<Grid item>
|
||||
<Alert severity="success">
|
||||
Pull requests have been successfully opened. You can start again to
|
||||
import more repositories
|
||||
{type === 'repo'
|
||||
? 'Pull requests have been successfully opened. You can start again to import more repositories'
|
||||
: 'Entity added to catalog successfully'}
|
||||
</Alert>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography className={classes.heading}>Pull request link:</Typography>
|
||||
<Link href={PRLink}>{PRLink}</Link>
|
||||
</Grid>
|
||||
<Grid item style={{ alignSelf: 'flex-end' }}>
|
||||
{type === 'repo' ? (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={PRLink}
|
||||
style={{ marginRight: '8px' }}
|
||||
>
|
||||
'View pull request on GitHub'
|
||||
</Link>
|
||||
) : null}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => nextStep({ reset: true })}
|
||||
>
|
||||
start again
|
||||
Start again
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -14,12 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RouteRef } from '@backstage/core';
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { ImportComponentPage } from './ImportComponentPage';
|
||||
|
||||
export const Router = () => (
|
||||
export const Router = ({ catalogRouteRef }: { catalogRouteRef: RouteRef }) => (
|
||||
<Routes>
|
||||
<Route element={<ImportComponentPage />} />
|
||||
<Route
|
||||
element={<ImportComponentPage catalogRouteRef={catalogRouteRef} />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ export function useGithubRepos() {
|
||||
const submitPrToRepo = async (selectedRepo: ConfigSpec) => {
|
||||
const token = await auth.getAccessToken(['repo']);
|
||||
|
||||
const [ownerName, repoName] = selectedRepo.repo.split('/').slice(-2);
|
||||
const [ownerName, repoName] = selectedRepo.location.split('/').slice(-2);
|
||||
const submitPRResponse = await api
|
||||
.submitPrToRepo({
|
||||
oAuthToken: token,
|
||||
@@ -55,5 +55,7 @@ export function useGithubRepos() {
|
||||
submitPrToRepo,
|
||||
generateEntityDefinitions: (repo: string) =>
|
||||
api.generateEntityDefinitions({ repo }),
|
||||
addLocation: (location: string) =>
|
||||
api.createRepositoryLocation({ location }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,15 +82,6 @@ export const ScaffolderPage = () => {
|
||||
color="primary"
|
||||
component={RouterLink}
|
||||
to="/catalog-import"
|
||||
style={{ marginRight: '8px' }}
|
||||
>
|
||||
Import Repository
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
component={RouterLink}
|
||||
to="/register-component"
|
||||
>
|
||||
Register Existing Component
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user