feat(scaffolder): working end-to-end flow with hardcoded params

This commit is contained in:
Ivan Shmidt
2020-06-29 22:42:39 +02:00
parent 97f3d6b056
commit dd5aa39d61
7 changed files with 204 additions and 55 deletions
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: {{cookiecutter.component_id}}
description: {{cookiecutter.description}}
spec:
type: website
lifecycle: experimental
owner: {{cookiecutter.owner}}
@@ -20,7 +20,6 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/config';
import { RequiredTemplateValues } from '../templater';
import { Repository, Remote, Signature, Cred } from 'nodegit';
import gitUrlParse from 'git-url-parse';
export class GithubStorer implements Storer {
private client: Octokit;
@@ -94,8 +94,9 @@ export async function createRouter(
entity: template,
values: {
component_id: `blob${Date.now()}`,
org: 'hojden',
org: 'shmidt-i-test',
description: 'test',
owner: 'somebody',
},
stages: [
{
@@ -16,26 +16,25 @@
import React, { useState } from 'react';
import useStaleWhileRevalidate from 'swr';
import { useParams } from 'react-router-dom';
import { LinearProgress, Button } from '@material-ui/core';
import { LinearProgress } from '@material-ui/core';
import { catalogApiRef } from '@backstage/plugin-catalog';
import {
useApi,
SimpleStepper,
SimpleStepperStep,
Page,
Content,
ContentHeader,
Header,
Lifecycle,
InfoCard,
} from '@backstage/core';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { withTheme, IChangeEvent } from '@rjsf/core';
import { Theme as MuiTheme } from '@rjsf/material-ui';
import {
TemplateEntityV1alpha1,
ComponentEntityV1alpha1,
} from '@backstage/catalog-model';
import { IChangeEvent } from '@rjsf/core';
import { JobStatusModal } from '../JobStatusModal';
import { scaffolderApiRef } from '../../api';
const Form = withTheme(MuiTheme);
import { MultistepJsonForm } from '../MultistepJsonForm';
import { Job } from '../JobStatusModal/types';
export const CreatePage = () => {
const catalogApi = useApi(catalogApiRef);
const scaffolderApi = useApi(scaffolderApiRef);
@@ -53,6 +52,7 @@ export const CreatePage = () => {
);
const [formState, setFormState] = useState({});
const handleFormReset = () => setFormState({});
const handleChange = (e: IChangeEvent) =>
setFormState({ ...formState, ...e.formData });
@@ -67,6 +67,22 @@ export const CreatePage = () => {
setJobId(job);
};
const [entity, setEntity] = React.useState<ComponentEntityV1alpha1 | null>(
null,
);
const handleCreateComplete = async (job: Job) => {
console.log('DEBUG:', { job });
const {
entities: [createdEntity],
} = await catalogApi.addLocation(
'github',
job.metadata.remoteUrl.replace(
/\.git$/,
'/blob/master/component-info.yaml',
),
);
setEntity(createdEntity);
};
return (
<Page>
<Header
@@ -79,47 +95,47 @@ export const CreatePage = () => {
subtitle="Create new software components using standard templates"
/>
<Content>
<ContentHeader title={template.metadata.title as string} />
{jobId && <JobStatusModal jobId={jobId} onClose={handleClose} />}
{/* <JSSONFormsStepper schemas={} */}
<SimpleStepper
onStepChange={(_prevStep, nextStep) => {
if (nextStep === 2) {
handleCreate();
}
}}
>
<SimpleStepperStep title="Configure your component">
<Form
formData={formState}
onChange={handleChange}
schema={{
$schema: 'http://json-schema.org/draft-07/schema#',
...template?.spec?.schema,
}}
>
<Button hidden />
</Form>
</SimpleStepperStep>
<SimpleStepperStep title="Choose repository">
<Form
formData={formState}
onChange={handleChange}
schema={{
$schema: 'http://json-schema.org/draft-07/schema#',
properties: {
repo: {
type: 'string',
description:
'Path to the repo where to upload created component',
{jobId && (
<JobStatusModal
onComplete={handleCreateComplete}
jobId={jobId}
onClose={handleClose}
entity={entity}
/>
)}
<InfoCard title={template.metadata.title as string} noPadding>
<MultistepJsonForm
formData={formState}
onChange={handleChange}
onReset={handleFormReset}
onFinish={handleCreate}
steps={[
{
label: 'Fill in template parameters',
schema: template.spec.schema,
},
{
label: 'Choose owner and repo',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
required: ['repo', 'owner'],
properties: {
owner: {
type: 'string',
title: 'Owner',
description: 'Who is going to own this component',
},
repo: {
type: 'string',
title: 'GitHub repository',
description: 'Repo where to upload created component',
},
},
},
}}
>
<Button hidden />
</Form>
</SimpleStepperStep>
</SimpleStepper>
},
]}
/>
</InfoCard>
</Content>
</Page>
);
@@ -1,21 +1,36 @@
import React from 'react';
import React, { useEffect } from 'react';
import {
Dialog,
LinearProgress,
DialogTitle,
DialogContent,
DialogActions,
} from '@material-ui/core';
import { JobStage } from './JobStage';
import { useJobPolling } from './useJobPolling';
import { Job } from './types';
import { ComponentEntityV1alpha1 } from '@backstage/catalog-model';
import { Button } from '@backstage/core';
import { entityRoute } from '@backstage/plugin-catalog';
import { generatePath } from 'react-router-dom';
type Props = {
onClose: () => void;
onComplete: (job: Job) => void;
jobId: string;
entity: ComponentEntityV1alpha1 | null;
};
export const JobStatusModal = ({ onClose, jobId }: Props) => {
console.log({ jobId });
export const JobStatusModal = ({
onClose,
jobId,
onComplete,
entity,
}: Props) => {
const job = useJobPolling(jobId);
useEffect(() => {
if (job?.status === 'COMPLETED') onComplete(job as Job);
}, [job]);
return (
<Dialog open onClose={onClose} fullWidth>
<DialogTitle id="responsive-dialog-title">
@@ -37,6 +52,23 @@ export const JobStatusModal = ({ onClose, jobId }: Props) => {
))
)}
</DialogContent>
{entity && (
<DialogActions>
<Button
to={generatePath(entityRoute.path, {
kind: entity.kind,
optionalNamespaceAndName: [
entity.metadata.namespace,
entity.metadata.name,
]
.filter(Boolean)
.join(':'),
})}
>
View in catalog
</Button>
</DialogActions>
)}
</Dialog>
);
};
@@ -0,0 +1,91 @@
import React, { useState } from 'react';
import { withTheme, FormProps, IChangeEvent } from '@rjsf/core';
import { Theme as MuiTheme } from '@rjsf/material-ui';
import {
Stepper,
Step,
StepLabel,
StepContent,
Button,
Paper,
Typography,
Box,
} from '@material-ui/core';
import { Content, StructuredMetadataTable } from '@backstage/core';
const Form = withTheme(MuiTheme);
type Step = {
schema: FormProps<any>['schema'];
label: string;
};
type Props = {
steps: Step[];
formData: Record<string, any>;
onChange: (e: IChangeEvent) => void;
onReset: () => void;
onFinish: () => void;
};
export const MultistepJsonForm = ({
steps,
formData,
onChange,
onReset,
onFinish,
}: Props) => {
const [activeStep, setActiveStep] = useState(0);
const handleReset = () => {
setActiveStep(0);
onReset();
};
const handleNext = () =>
setActiveStep(Math.min(activeStep + 1, steps.length));
const handleBack = () => setActiveStep(Math.max(activeStep - 1, 0));
return (
<>
<Stepper activeStep={activeStep} orientation="vertical">
{steps.map(({ label, schema }) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
<StepContent>
<Form
noHtml5Validate
formData={formData}
onChange={onChange}
schema={schema}
onSubmit={e => {
if (e.errors.length === 0) handleNext();
}}
>
<div>
<div>
<Button disabled={activeStep === 0} onClick={handleBack}>
Back
</Button>
<Button variant="contained" color="primary" type="submit">
{activeStep === steps.length - 1 ? 'Finish' : 'Next'}
</Button>
</div>
</div>
</Form>
</StepContent>
</Step>
))}
</Stepper>
{activeStep === steps.length && (
<Content>
<Paper square elevation={0}>
<Typography variant="h6">Review and create</Typography>
<StructuredMetadataTable dense metadata={formData} />
<Box mb={4} />
<Button onClick={handleBack}>Back</Button>
<Button onClick={handleReset}>Reset</Button>
<Button variant="contained" color="primary" onClick={onFinish}>
Create
</Button>
</Paper>
</Content>
)}
</>
);
};
@@ -0,0 +1 @@
export { MultistepJsonForm } from './MultistepJsonForm';