fix(scaffolder): big cleanup
This commit is contained in:
@@ -34,10 +34,18 @@ export interface CatalogApi {
|
||||
getEntityByName(
|
||||
compoundName: EntityCompoundName,
|
||||
): Promise<Entity | undefined>;
|
||||
getEntities(filter?: Record<string, string | string[]>): Promise<Entity[]>;
|
||||
addLocation(type: string, target: string): Promise<AddLocationResponse>;
|
||||
getEntities<EntityType extends Entity = Entity>(
|
||||
filter?: Record<string, string | string[]>,
|
||||
): Promise<EntityType[]>;
|
||||
addLocation<EntityType extends Entity = Entity>(
|
||||
type: string,
|
||||
target: string,
|
||||
): Promise<AddLocationResponse<EntityType>>;
|
||||
getLocationByEntity(entity: Entity): Promise<Location | undefined>;
|
||||
removeEntityByUid(uid: string): Promise<void>;
|
||||
}
|
||||
|
||||
export type AddLocationResponse = { location: Location; entities: Entity[] };
|
||||
export type AddLocationResponse<EntityType extends Entity = Entity> = {
|
||||
location: Location;
|
||||
entities: EntityType[];
|
||||
};
|
||||
|
||||
@@ -25,15 +25,17 @@
|
||||
"@backstage/core": "^0.1.1-alpha.12",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.12",
|
||||
"@backstage/theme": "^0.1.1-alpha.12",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.12",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.12",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@rjsf/core": "^2.1.0",
|
||||
"@rjsf/material-ui": "^2.1.0",
|
||||
"classnames": "^2.2.6",
|
||||
"moment": "^2.27.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-lazylog": "^4.5.2",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "6.0.0-alpha.5",
|
||||
"react-use": "^14.2.0",
|
||||
"swr": "^0.2.2"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { CreatePage } from './CreatePage';
|
||||
+33
-46
@@ -13,21 +13,24 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useEffect, useState, Suspense } from 'react';
|
||||
import {
|
||||
Box,
|
||||
ExpansionPanel,
|
||||
ExpansionPanelSummary,
|
||||
Typography,
|
||||
ExpansionPanelDetails,
|
||||
ExpansionPanelSummary,
|
||||
LinearProgress,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import moment from 'moment';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Job } from './types';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import cn from 'classnames';
|
||||
import moment from 'moment';
|
||||
import React, { Suspense, useEffect, useState } from 'react';
|
||||
import { Job } from '../../types';
|
||||
|
||||
const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog'));
|
||||
moment.relativeTimeThreshold('ss', 0);
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
expansionPanelDetails: {
|
||||
padding: 0,
|
||||
@@ -37,37 +40,10 @@ const useStyles = makeStyles(theme => ({
|
||||
marginRight: 0,
|
||||
marginLeft: '-20px',
|
||||
},
|
||||
neutral: {},
|
||||
failed: {
|
||||
position: 'relative',
|
||||
'&:after': {
|
||||
pointerEvents: 'none',
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
boxShadow: `inset 4px 0px 0px ${theme.palette.error.main}`,
|
||||
},
|
||||
},
|
||||
running: {
|
||||
position: 'relative',
|
||||
'&:after': {
|
||||
pointerEvents: 'none',
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
boxShadow: `inset 4px 0px 0px ${theme.palette.info.main}`,
|
||||
},
|
||||
},
|
||||
cardContent: {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
success: {
|
||||
expansionPanel: {
|
||||
position: 'relative',
|
||||
'&:after': {
|
||||
pointerEvents: 'none',
|
||||
@@ -77,6 +53,21 @@ const useStyles = makeStyles(theme => ({
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
neutral: {},
|
||||
failed: {
|
||||
'&:after': {
|
||||
boxShadow: `inset 4px 0px 0px ${theme.palette.error.main}`,
|
||||
},
|
||||
},
|
||||
started: {
|
||||
'&:after': {
|
||||
boxShadow: `inset 4px 0px 0px ${theme.palette.info.main}`,
|
||||
},
|
||||
},
|
||||
completed: {
|
||||
'&:after': {
|
||||
boxShadow: `inset 4px 0px 0px ${theme.palette.success.main}`,
|
||||
},
|
||||
},
|
||||
@@ -90,13 +81,14 @@ type Props = {
|
||||
endedAt?: string;
|
||||
status?: Job['status'];
|
||||
};
|
||||
|
||||
export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
useEffect(() => {
|
||||
if (status === 'FAILED') setExpanded(true);
|
||||
}, [status === 'FAILED', setExpanded]);
|
||||
}, [status, setExpanded]);
|
||||
|
||||
const timeElapsed =
|
||||
status !== 'PENDING'
|
||||
@@ -108,15 +100,10 @@ export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => {
|
||||
return (
|
||||
<ExpansionPanel
|
||||
TransitionProps={{ unmountOnExit: true }}
|
||||
className={
|
||||
status === 'STARTED'
|
||||
? classes.running
|
||||
: status === 'FAILED'
|
||||
? classes.failed
|
||||
: status === 'COMPLETED'
|
||||
? classes.success
|
||||
: classes.neutral
|
||||
}
|
||||
className={cn(
|
||||
classes.expansionPanel,
|
||||
classes[status.toLowerCase()] ?? classes.neutral,
|
||||
)}
|
||||
expanded={expanded}
|
||||
onChange={(_, newState) => setExpanded(newState)}
|
||||
>
|
||||
@@ -134,11 +121,11 @@ export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => {
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails className={classes.expansionPanelDetails}>
|
||||
{log.length === 0 ? (
|
||||
'Nothing here...'
|
||||
<Box px={4}>No logs available for this step</Box>
|
||||
) : (
|
||||
<Suspense fallback={<LinearProgress />}>
|
||||
<div style={{ height: '20vh', width: '100%' }}>
|
||||
<LazyLog text={log.join('\n')} extraLines={1} enableSearch />
|
||||
<LazyLog text={log.join('\n')} extraLines={1} />
|
||||
</div>
|
||||
</Suspense>
|
||||
)}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { JobStage } from './JobStage';
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -6,13 +21,14 @@ import {
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from '@material-ui/core';
|
||||
import { JobStage } from './JobStage';
|
||||
import { JobStage } from '../JobStage/JobStage';
|
||||
import { useJobPolling } from './useJobPolling';
|
||||
import { Job } from './types';
|
||||
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;
|
||||
@@ -29,8 +45,9 @@ export const JobStatusModal = ({
|
||||
const job = useJobPolling(jobId);
|
||||
|
||||
useEffect(() => {
|
||||
if (job?.status === 'COMPLETED') onComplete(job as Job);
|
||||
}, [job]);
|
||||
if (job?.status === 'COMPLETED') onComplete(job);
|
||||
}, [job, onComplete]);
|
||||
|
||||
return (
|
||||
<Dialog open onClose={onClose} fullWidth>
|
||||
<DialogTitle id="responsive-dialog-title">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Job } from './types';
|
||||
import { Job } from '../../types';
|
||||
import { useApi } from '@backstage/core';
|
||||
import { scaffolderApiRef } from '../../api';
|
||||
|
||||
@@ -42,6 +42,7 @@ export const useJobPolling = (
|
||||
|
||||
useEffect(() => {
|
||||
if (!jobId) return () => {};
|
||||
|
||||
const stopPolling = poll(async () => {
|
||||
const nextJobState = await scaffolderApi.getJob(jobId);
|
||||
if (
|
||||
|
||||
@@ -13,34 +13,39 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { withTheme, IChangeEvent, FormProps } from '@rjsf/core';
|
||||
import { Theme as MuiTheme } from '@rjsf/material-ui';
|
||||
import { JSONSchema } from '@backstage/catalog-model';
|
||||
import { Content, StructuredMetadataTable } from '@backstage/core';
|
||||
import {
|
||||
Stepper,
|
||||
Step,
|
||||
StepLabel,
|
||||
StepContent,
|
||||
Box,
|
||||
Button,
|
||||
Paper,
|
||||
Step,
|
||||
StepContent,
|
||||
StepLabel,
|
||||
Stepper,
|
||||
Typography,
|
||||
Box,
|
||||
} from '@material-ui/core';
|
||||
import { Content, StructuredMetadataTable } from '@backstage/core';
|
||||
import { JSONSchema } from '@backstage/catalog-model';
|
||||
import { FormProps, IChangeEvent, withTheme } from '@rjsf/core';
|
||||
import { Theme as MuiTheme } from '@rjsf/material-ui';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const Form = withTheme(MuiTheme);
|
||||
type Step = {
|
||||
schema: JSONSchema;
|
||||
label: string;
|
||||
};
|
||||
} & Partial<FormProps>;
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Steps for the form, each contains label and form schema
|
||||
*/
|
||||
steps: Step[];
|
||||
formData: Record<string, any>;
|
||||
onChange: (e: IChangeEvent) => void;
|
||||
onReset: () => void;
|
||||
onFinish: () => void;
|
||||
};
|
||||
|
||||
export const MultistepJsonForm = ({
|
||||
steps,
|
||||
formData,
|
||||
@@ -57,10 +62,11 @@ export const MultistepJsonForm = ({
|
||||
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 }) => (
|
||||
{steps.map(({ label, schema, ...formProps }) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
<StepContent>
|
||||
@@ -72,17 +78,14 @@ export const MultistepJsonForm = ({
|
||||
onSubmit={e => {
|
||||
if (e.errors.length === 0) handleNext();
|
||||
}}
|
||||
{...formProps}
|
||||
>
|
||||
<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>
|
||||
<Button disabled={activeStep === 0} onClick={handleBack}>
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="contained" color="primary" type="submit">
|
||||
Next step
|
||||
</Button>
|
||||
</Form>
|
||||
</StepContent>
|
||||
</Step>
|
||||
|
||||
+21
-18
@@ -14,31 +14,43 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import {
|
||||
Lifecycle,
|
||||
Content,
|
||||
ContentHeader,
|
||||
errorApiRef,
|
||||
Header,
|
||||
SupportButton,
|
||||
Lifecycle,
|
||||
Page,
|
||||
pageTheme,
|
||||
SupportButton,
|
||||
useApi,
|
||||
errorApiRef,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import {
|
||||
Typography,
|
||||
Link,
|
||||
Button,
|
||||
Grid,
|
||||
LinearProgress,
|
||||
Link,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import TemplateCard from './TemplateCard';
|
||||
import useStaleWhileRevalidate from 'swr';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { TemplateCard, TemplateCardProps } from '../TemplateCard';
|
||||
|
||||
const getTemplateCardProps = (
|
||||
template: TemplateEntityV1alpha1,
|
||||
): TemplateCardProps & { key: string } => {
|
||||
return {
|
||||
key: template.metadata.uid!,
|
||||
name: template.metadata.name,
|
||||
title: `${(template.metadata.title || template.metadata.name) ?? ''}`,
|
||||
type: template.spec.type ?? '',
|
||||
description: template.metadata.description ?? '-',
|
||||
tags: (template.metadata?.tags as string[]) ?? [],
|
||||
};
|
||||
};
|
||||
export const ScaffolderPage: React.FC<{}> = () => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
@@ -96,16 +108,7 @@ export const ScaffolderPage: React.FC<{}> = () => {
|
||||
templates.map(template => {
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TemplateCard
|
||||
key={template.metadata.uid}
|
||||
name={template.metadata.name}
|
||||
title={`${
|
||||
(template.metadata.title || template.metadata.name) ?? ''
|
||||
}`}
|
||||
type={template.spec.type ?? ''}
|
||||
description={template.metadata.description ?? '-'}
|
||||
tags={(template.metadata?.tags as string[]) ?? []}
|
||||
/>
|
||||
<TemplateCard {...getTemplateCardProps(template)} />
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { ScaffolderPage } from './ScaffolderPage';
|
||||
+7
-8
@@ -13,11 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Button } from '@backstage/core';
|
||||
import { Card, Chip, makeStyles, Typography } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Button } from '@backstage/core';
|
||||
import { Card, Chip, Typography, makeStyles } from '@material-ui/core';
|
||||
import { createTemplateRoute } from '../../routes';
|
||||
import { templateRoute } from '../../routes';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
header: {
|
||||
@@ -40,14 +40,15 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
type TemplateCardProps = {
|
||||
export type TemplateCardProps = {
|
||||
description: string;
|
||||
tags: string[];
|
||||
title: string;
|
||||
type: string;
|
||||
name: string;
|
||||
};
|
||||
const TemplateCard = ({
|
||||
|
||||
export const TemplateCard = ({
|
||||
description,
|
||||
tags,
|
||||
title,
|
||||
@@ -55,7 +56,7 @@ const TemplateCard = ({
|
||||
name,
|
||||
}: TemplateCardProps) => {
|
||||
const classes = useStyles();
|
||||
const href = generatePath(createTemplateRoute.path, { templateName: name });
|
||||
const href = generatePath(templateRoute.path, { templateName: name });
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -79,5 +80,3 @@ const TemplateCard = ({
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateCard;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { TemplateCard } from './TemplateCard';
|
||||
export type { TemplateCardProps } from './TemplateCard';
|
||||
+103
-67
@@ -13,45 +13,76 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import useStaleWhileRevalidate from 'swr';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { LinearProgress } from '@material-ui/core';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import {
|
||||
useApi,
|
||||
Page,
|
||||
Content,
|
||||
Header,
|
||||
Lifecycle,
|
||||
InfoCard,
|
||||
errorApiRef,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
TemplateEntityV1alpha1,
|
||||
ComponentEntityV1alpha1,
|
||||
TemplateEntityV1alpha1,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
Content,
|
||||
errorApiRef,
|
||||
Header,
|
||||
InfoCard,
|
||||
Lifecycle,
|
||||
Page,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import { LinearProgress } from '@material-ui/core';
|
||||
import { IChangeEvent } from '@rjsf/core';
|
||||
import { JobStatusModal } from '../JobStatusModal';
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useStaleWhileRevalidate from 'swr';
|
||||
import { scaffolderApiRef } from '../../api';
|
||||
import { JobStatusModal } from '../JobStatusModal';
|
||||
import { Job } from '../../types';
|
||||
import { MultistepJsonForm } from '../MultistepJsonForm';
|
||||
import { Job } from '../JobStatusModal/types';
|
||||
export const CreatePage = () => {
|
||||
import { Navigate } from 'react-router';
|
||||
import { rootRoute } from '../../routes';
|
||||
|
||||
const useTemplate = (
|
||||
templateName: string,
|
||||
catalogApi: typeof catalogApiRef.T,
|
||||
) => {
|
||||
const { data, error } = useStaleWhileRevalidate(
|
||||
`templates/${templateName}`,
|
||||
async () =>
|
||||
catalogApi.getEntities<TemplateEntityV1alpha1>({
|
||||
kind: 'Template',
|
||||
'metadata.name': templateName,
|
||||
}),
|
||||
);
|
||||
return { template: data?.[0], loading: !error && !data, error };
|
||||
};
|
||||
|
||||
const OWNER_REPO_SCHEMA = {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#' as const,
|
||||
required: ['storePath', 'owner'],
|
||||
properties: {
|
||||
owner: {
|
||||
type: 'string' as const,
|
||||
title: 'Owner',
|
||||
description: 'Who is going to own this component',
|
||||
},
|
||||
storePath: {
|
||||
format: 'GitHub user or org / Repo name',
|
||||
type: 'string' as const,
|
||||
title: 'Store path',
|
||||
description: 'GitHub store path in org/repo format',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const REPO_FORMAT = {
|
||||
'GitHub user or org / Repo name': /[^\/]*\/[^\/]*/,
|
||||
};
|
||||
|
||||
export const TemplatePage = () => {
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
const { templateName } = useParams();
|
||||
const {
|
||||
data: [template] = [] as TemplateEntityV1alpha1[],
|
||||
isValidating,
|
||||
} = useStaleWhileRevalidate(
|
||||
`templates/${templateName}`,
|
||||
async () =>
|
||||
(catalogApi.getEntities({
|
||||
kind: 'Template',
|
||||
'metadata.name': templateName,
|
||||
}) as any) as Promise<TemplateEntityV1alpha1[]>,
|
||||
);
|
||||
const { template, loading } = useTemplate(templateName, catalogApi);
|
||||
|
||||
const [formState, setFormState] = useState({});
|
||||
|
||||
const handleFormReset = () => setFormState({});
|
||||
@@ -62,22 +93,24 @@ export const CreatePage = () => {
|
||||
const handleClose = () => setJobId(null);
|
||||
|
||||
const handleCreate = async () => {
|
||||
const job = await scaffolderApi.scaffold(template, formState);
|
||||
const job = await scaffolderApi.scaffold(template!, formState);
|
||||
setJobId(job);
|
||||
};
|
||||
|
||||
const [entity, setEntity] = React.useState<ComponentEntityV1alpha1 | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleCreateComplete = async (job: Job) => {
|
||||
const componentYaml = job.metadata.remoteUrl?.replace(
|
||||
/\.git$/,
|
||||
'/blob/master/component-info.yaml',
|
||||
);
|
||||
|
||||
if (!componentYaml) {
|
||||
errorApi.post(
|
||||
new Error(
|
||||
`Failed to find component-info.yaml file in ${job.metadata.remoteUrl}`,
|
||||
`Failed to find component-info.yaml file in ${job.metadata.remoteUrl}.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -85,13 +118,27 @@ export const CreatePage = () => {
|
||||
|
||||
const {
|
||||
entities: [createdEntity],
|
||||
} = await catalogApi.addLocation('github', componentYaml);
|
||||
} = await catalogApi.addLocation<ComponentEntityV1alpha1>(
|
||||
'github',
|
||||
componentYaml,
|
||||
);
|
||||
|
||||
setEntity(createdEntity as ComponentEntityV1alpha1);
|
||||
setEntity(createdEntity);
|
||||
};
|
||||
|
||||
if (!template && isValidating) return <LinearProgress />;
|
||||
if (!template || !template?.spec?.schema) return null;
|
||||
if (!loading && !template) {
|
||||
errorApi.post(new Error('Template was not found.'));
|
||||
return <Navigate to={rootRoute.path} />;
|
||||
}
|
||||
|
||||
if (template && !template?.spec?.schema) {
|
||||
errorApi.post(
|
||||
new Error(
|
||||
'Template schema is corrupted, please check the template.yaml file.',
|
||||
),
|
||||
);
|
||||
return <Navigate to={rootRoute.path} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
@@ -105,6 +152,7 @@ export const CreatePage = () => {
|
||||
subtitle="Create new software components using standard templates"
|
||||
/>
|
||||
<Content>
|
||||
{loading && <LinearProgress />}
|
||||
{jobId && (
|
||||
<JobStatusModal
|
||||
onComplete={handleCreateComplete}
|
||||
@@ -113,39 +161,27 @@ export const CreatePage = () => {
|
||||
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: ['storePath', 'owner'],
|
||||
properties: {
|
||||
owner: {
|
||||
type: 'string',
|
||||
title: 'Owner',
|
||||
description: 'Who is going to own this component',
|
||||
},
|
||||
storePath: {
|
||||
type: 'string',
|
||||
title: 'Store path',
|
||||
description: 'GitHub store path in org/repo format',
|
||||
},
|
||||
},
|
||||
{template && (
|
||||
<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,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</InfoCard>
|
||||
{
|
||||
label: 'Choose owner and repo',
|
||||
schema: OWNER_REPO_SCHEMA,
|
||||
customFormats: REPO_FORMAT,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</InfoCard>
|
||||
)}
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { TemplatePage } from './TemplatePage';
|
||||
@@ -15,4 +15,4 @@
|
||||
*/
|
||||
|
||||
export { plugin } from './plugin';
|
||||
export { rootRoute, createTemplateRoute } from './routes';
|
||||
export { rootRoute, templateRoute } from './routes';
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import { ScaffolderPage } from './components/ScaffolderPage';
|
||||
import { CreatePage } from './components/CreatePage';
|
||||
import { rootRoute, createTemplateRoute } from './routes';
|
||||
import { TemplatePage } from './components/TemplatePage';
|
||||
import { rootRoute, templateRoute } from './routes';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'scaffolder',
|
||||
register({ router }) {
|
||||
router.addRoute(rootRoute, ScaffolderPage);
|
||||
router.addRoute(createTemplateRoute, CreatePage);
|
||||
router.addRoute(templateRoute, TemplatePage);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { createRouteRef } from '@backstage/core';
|
||||
|
||||
export const rootRoute = createRouteRef({
|
||||
@@ -5,7 +20,7 @@ export const rootRoute = createRouteRef({
|
||||
path: '/create',
|
||||
title: 'Create new entity',
|
||||
});
|
||||
export const createTemplateRoute = createRouteRef({
|
||||
export const templateRoute = createRouteRef({
|
||||
icon: () => null,
|
||||
path: '/create/:templateName',
|
||||
title: 'Entity creation',
|
||||
|
||||
@@ -13030,6 +13030,11 @@ moment@2.26.0, moment@^2.25.3, moment@^2.26.0:
|
||||
resolved "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
|
||||
integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
|
||||
|
||||
moment@^2.27.0:
|
||||
version "2.27.0"
|
||||
resolved "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
|
||||
integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==
|
||||
|
||||
monaco-editor@^0.20.0:
|
||||
version "0.20.0"
|
||||
resolved "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea"
|
||||
@@ -15697,7 +15702,7 @@ react-router-dom@^5.2.0:
|
||||
tiny-invariant "^1.0.2"
|
||||
tiny-warning "^1.0.0"
|
||||
|
||||
react-router@5.2.0:
|
||||
react-router@5.2.0, react-router@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
|
||||
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==
|
||||
|
||||
Reference in New Issue
Block a user