Merge branch 'master' of github.com:spotify/backstage into mob/techdocs-sanitize-html

This commit is contained in:
Sebastian Qvarfordt
2020-07-07 10:45:50 +02:00
41 changed files with 1447 additions and 81 deletions
+12 -10
View File
@@ -15,18 +15,20 @@
For more information go to [backstage.io](https://backstage.io) or join our [Discord chatroom](https://discord.gg/EBHEGzX).
### Features
* Create and manage all of your organizations software and microservices in one place.
* Services catalog keeps track of all software and its ownership.
* Visualizations provide information about your backend services and tooling, and help you monitor them.
* A unified method for managing microservices offers both visibility and control.
* Preset templates allow engineers to quickly create microservices in a standardized way ([coming soon](https://github.com/spotify/backstage/milestone/11)).
* Centralized, full-featured technical documentation with integrated tooling that makes it easy for developers to set up, publish, and maintain alongside their code ([coming soon](https://github.com/spotify/backstage/milestone/15)).
- Create and manage all of your organizations software and microservices in one place.
- Services catalog keeps track of all software and its ownership.
- Visualizations provide information about your backend services and tooling, and help you monitor them.
- A unified method for managing microservices offers both visibility and control.
- Preset templates allow engineers to quickly create microservices in a standardized way ([coming soon](https://github.com/spotify/backstage/milestone/11)).
- Centralized, full-featured technical documentation with integrated tooling that makes it easy for developers to set up, publish, and maintain alongside their code ([coming soon](https://github.com/spotify/backstage/milestone/15)).
### Benefits
* For _engineering managers_, it allows you to maintain standards and best practices across the organization, and can help you manage your whole tech ecosystem, from migrations to test certification.
* For _end users_ (developers), it makes it fast and simple to build software components in a standardized way, and it provides a central place to manage all projects and documentation.
* For _platform engineers_, it enables extensibility and scalability by letting you easily integrate new tools and services (via plugins), as well as extending the functionality of existing ones.
* For _everyone_, its a single, consistent experience that ties all your infrastructure tooling, resources, standards, owners, contributors, and administrators together in one place.
- For _engineering managers_, it allows you to maintain standards and best practices across the organization, and can help you manage your whole tech ecosystem, from migrations to test certification.
- For _end users_ (developers), it makes it fast and simple to build software components in a standardized way, and it provides a central place to manage all projects and documentation.
- For _platform engineers_, it enables extensibility and scalability by letting you easily integrate new tools and services (via plugins), as well as extending the functionality of existing ones.
- For _everyone_, its a single, consistent experience that ties all your infrastructure tooling, resources, standards, owners, contributors, and administrators together in one place.
## Backstage Service Catalog (alpha)
@@ -2,39 +2,46 @@
## Context
Facebook has removed `React.FC` from their base template for a Typescript project. The reason for this was that it was found to be an unnecessary feature with next to no benefits in combination with a few downsides.
Facebook has removed `React.FC` from their base template for a Typescript
project. The reason for this was that it was found to be an unnecessary feature
with next to no benefits in combination with a few downsides.
The main reasons were:
- **children props** were implicitly added
- **Generic Type** were not supported on children
Read more about the removal in [this PR](https://github.com/facebook/create-react-app/pull/8177).
Read more about the removal in
[this PR](https://github.com/facebook/create-react-app/pull/8177).
## Decision
To keep our codebase up to date, we have decided that `React.FC` and `React.SFC` should be avoided in our codebase when adding new code.
To keep our codebase up to date, we have decided that `React.FC` and `React.SFC`
should be avoided in our codebase when adding new code.
Here is an example:
```ts
/* Avoid this: */
type BadProps = { text: string; };
type BadProps = { text: string };
const BadComponent: FC<BadProps> = ({ text, children }) => (
<div>
<div>{text}</div>
{children}
</div>
)
);
/* Do this instead: */
type GoodProps = { text: string; children?: React.ReactNode; };
type GoodProps = { text: string; children?: React.ReactNode };
const GoodComponent = ({ text, children }: GoodProps) => (
<div>
<div>{text}</div>
{children}
</div>
)
);
```
## Consequences
We will gradually remove the current usage of `React.FC` and `React.SFC` from our codebase.
We will gradually remove the current usage of `React.FC` and `React.SFC` from
our codebase.
+1 -1
View File
@@ -33,7 +33,7 @@
"version": "1.0.0",
"devDependencies": {
"@spotify/eslint-config-oss": "^1.0.1",
"@spotify/prettier-config": "^7.0.0",
"@spotify/prettier-config": "^8.0.0",
"husky": "^4.2.3",
"lerna": "^3.20.2",
"lint-staged": "^10.1.0",
+9
View File
@@ -55,6 +55,7 @@ import {
graphQlBrowseApiRef,
GraphQLEndpoints,
} from '@backstage/plugin-graphiql';
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
@@ -142,6 +143,14 @@ export const apis = (config: ConfigApi) => {
}),
);
builder.add(
scaffolderApiRef,
new ScaffolderApi({
apiOrigin: backendUrl,
basePath: '/scaffolder/v1',
}),
);
builder.add(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008'));
builder.add(
+2
View File
@@ -21,7 +21,9 @@
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.12",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.28.2",
"json-schema": "^0.2.5",
"lodash": "^4.17.15",
"uuid": "^8.0.0",
"yup": "^0.29.1"
+1 -1
View File
@@ -18,5 +18,5 @@ export * from './entity';
export { EntityPolicies } from './EntityPolicies';
export * from './kinds';
export * from './location';
export type { EntityPolicy } from './types';
export type { EntityPolicy, JSONSchema } from './types';
export * from './validation';
@@ -33,6 +33,22 @@ describe('TemplateEntityV1alpah1', () => {
},
spec: {
type: 'cookiecutter',
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',
},
},
},
},
};
policy = new TemplateEntityV1alpha1Policy();
@@ -16,7 +16,7 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import type { EntityPolicy } from '../types';
import type { EntityPolicy, JSONSchema } from '../types';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'Template' as const;
@@ -27,6 +27,7 @@ export interface TemplateEntityV1alpha1 extends Entity {
spec: {
type: string;
path?: string;
schema: JSONSchema;
};
}
@@ -41,6 +42,7 @@ export class TemplateEntityV1alpha1Policy implements EntityPolicy {
.object({
type: yup.string().required().min(1),
path: yup.string(),
schema: yup.object().required(),
})
.required(),
});
+4
View File
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { JsonValue } from '@backstage/config';
import { JSONSchema7 } from 'json-schema';
import type { Entity } from './entity/Entity';
/**
@@ -30,3 +32,5 @@ export type EntityPolicy = {
*/
enforce(entity: Entity): Promise<Entity>;
};
export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue };
@@ -13,7 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { Children, isValidElement, FC, useState } from 'react';
import React, {
Children,
isValidElement,
FC,
useState,
useEffect,
} from 'react';
import { Stepper as MuiStepper } from '@material-ui/core';
type InternalState = {
@@ -38,16 +44,22 @@ export const VerticalStepperContext = React.createContext<InternalState>({
export interface StepperProps {
elevated?: boolean;
onStepChange?: (prevIndex: number, nextIndex: number) => void;
activeStep?: number;
}
export const SimpleStepper: FC<StepperProps> = ({
children,
elevated,
onStepChange,
activeStep = 0,
}) => {
const [stepIndex, setStepIndex] = useState<number>(0);
const [stepIndex, setStepIndex] = useState<number>(activeStep);
const [stepHistory, setStepHistory] = useState<number[]>([0]);
useEffect(() => {
setStepIndex(activeStep);
}, [activeStep]);
const steps: React.ReactNode[] = [];
let endStep;
Children.forEach(children, child => {
+4 -1
View File
@@ -40,4 +40,7 @@ export interface CatalogApi {
removeEntityByUid(uid: string): Promise<void>;
}
export type AddLocationResponse = { location: Location; entities: Entity[] };
export type AddLocationResponse = {
location: Location;
entities: Entity[];
};
@@ -11,3 +11,17 @@ spec:
processor: cookiecutter
type: website
path: '.'
schema:
required:
- component_id
- description
properties:
component_id:
title: Name
type: string
description: Unique name of the component
description:
title: Description
type: string
description: Description of the component
@@ -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}}
@@ -11,3 +11,16 @@ spec:
processor: cookiecutter
type: service
path: '.'
schema:
required:
- component_id
- description
properties:
component_id:
title: Name
type: string
description: Unique name of the component
description:
title: Description
type: string
description: Description of the component
@@ -39,6 +39,22 @@ describe('JobProcessor', () => {
spec: {
type: 'cookiecutter',
path: './template',
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',
},
},
},
},
};
@@ -98,6 +98,7 @@ export class JobProcessor implements Processor {
try {
// Run the handler with the context created for the Job and some
// Additional logging helpers.
stage.status = 'STARTED';
const handlerResponse = await stage.handler({
...job.context,
logger,
@@ -49,6 +49,22 @@ describe('GitHubPreparer', () => {
spec: {
type: 'cookiecutter',
path: './template',
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',
},
},
},
},
};
});
@@ -41,6 +41,22 @@ describe('Helpers', () => {
spec: {
type: 'cookiecutter',
path: './template',
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',
},
},
},
},
};
@@ -72,6 +88,22 @@ describe('Helpers', () => {
spec: {
type: 'cookiecutter',
path: './template',
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',
},
},
},
},
};
@@ -101,6 +133,22 @@ describe('Helpers', () => {
spec: {
type: 'cookiecutter',
path: './template',
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',
},
},
},
},
};
@@ -131,6 +179,22 @@ describe('Helpers', () => {
spec: {
type: 'cookiecutter',
path: './template',
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',
},
},
},
},
};
@@ -159,6 +223,22 @@ describe('Helpers', () => {
spec: {
type: 'cookiecutter',
path: './template',
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',
},
},
},
},
};
@@ -37,6 +37,22 @@ describe('Preparers', () => {
spec: {
type: 'cookiecutter',
path: '.',
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',
},
},
},
},
};
it('should throw an error when the preparer for the source location is not registered', () => {
@@ -74,6 +90,22 @@ describe('Preparers', () => {
spec: {
type: 'cookiecutter',
path: '.',
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',
},
},
},
},
};
@@ -14,22 +14,21 @@
* limitations under the License.
*/
import { Logger } from 'winston';
import Router from 'express-promise-router';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/config';
import { Octokit } from '@octokit/rest';
import Docker from 'dockerode';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import {
PreparerBuilder,
TemplaterBase,
GithubPublisher,
JobProcessor,
PreparerBuilder,
RequiredTemplateValues,
StageContext,
GithubPublisher,
TemplaterBase,
} from '../scaffolder';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import Docker from 'dockerode';
import {} from '@backstage/backend-common';
import { Octokit } from '@octokit/rest';
import { JsonValue } from '@backstage/config';
export interface RouterOptions {
preparers: PreparerBuilder;
@@ -107,7 +106,7 @@ export async function createRouter(
{
name: 'Run the templater',
handler: async (ctx: StageContext<{ skeletonDir: string }>) => {
const resultDir = await templater.run({
const { resultDir } = await templater.run({
directory: ctx.skeletonDir,
dockerClient,
logStream: ctx.logStream,
+8 -1
View File
@@ -22,14 +22,20 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.12",
"@backstage/plugin-catalog": "^0.1.1-alpha.12",
"@backstage/core": "^0.1.1-alpha.12",
"@backstage/plugin-catalog": "^0.1.1-alpha.12",
"@backstage/theme": "^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.26.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-lazylog": "^4.5.2",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^14.2.0",
"swr": "^0.2.2"
@@ -37,6 +43,7 @@
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.12",
"@backstage/dev-utils": "^0.1.1-alpha.12",
"@backstage/test-utils": "^0.1.1-alpha.12",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+73
View File
@@ -0,0 +1,73 @@
/*
* 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 { createApiRef } from '@backstage/core';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
export const scaffolderApiRef = createApiRef<ScaffolderApi>({
id: 'plugin.scaffolder.service',
description: 'Used to make requests towards the scaffolder backend',
});
export class ScaffolderApi {
private apiOrigin: string;
private basePath: string;
constructor({
apiOrigin,
basePath,
}: {
apiOrigin: string;
basePath: string;
}) {
this.apiOrigin = apiOrigin;
this.basePath = basePath;
}
/**
*
* @param template Template entity for the scaffolder to use. New project is going to be created out of this template.
* @param values Parameters for the template, e.g. name, description
*/
async scaffold(
template: TemplateEntityV1alpha1,
values: Record<string, any>,
) {
const url = `${this.apiOrigin}${this.basePath}/jobs`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
// TODO(shmidt-i): when repo picker is implemented, take isOrg from it
body: JSON.stringify({ template, values: { ...values, isOrg: true } }),
});
if (response.status !== 201) {
throw new Error(await response.text());
}
const { id } = await response.json();
return id;
}
async getJob(jobId: string) {
const url = `${this.apiOrigin}${this.basePath}/job/${encodeURIComponent(
jobId,
)}`;
return fetch(url).then(x => x.json());
}
}
@@ -0,0 +1,136 @@
/*
* 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 {
Box,
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
LinearProgress,
Typography,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
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,
},
button: {
order: -1,
marginRight: 0,
marginLeft: '-20px',
},
cardContent: {
backgroundColor: theme.palette.background.default,
},
expansionPanel: {
position: 'relative',
'&:after': {
pointerEvents: 'none',
content: '""',
position: 'absolute',
top: 0,
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}`,
},
},
}));
type Props = {
name: string;
className?: string;
log: string[];
startedAt: string;
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, setExpanded]);
const timeElapsed =
status !== 'PENDING'
? moment
.duration(moment(endedAt ?? moment()).diff(moment(startedAt)))
.humanize()
: null;
return (
<ExpansionPanel
TransitionProps={{ unmountOnExit: true }}
className={cn(
classes.expansionPanel,
classes[status.toLowerCase() as keyof ReturnType<typeof useStyles>] ??
classes.neutral,
)}
expanded={expanded}
onChange={(_, newState) => setExpanded(newState)}
>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`panel-${name}-content`}
id={`panel-${name}-header`}
IconButtonProps={{
className: classes.button,
}}
>
<Typography variant="button">
{name} {timeElapsed && `(${timeElapsed})`}
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.expansionPanelDetails}>
{log.length === 0 ? (
<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} />
</div>
</Suspense>
)}
</ExpansionPanelDetails>
</ExpansionPanel>
);
};
@@ -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';
@@ -0,0 +1,91 @@
/*
* 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,
LinearProgress,
DialogTitle,
DialogContent,
DialogActions,
} from '@material-ui/core';
import { JobStage } from '../JobStage/JobStage';
import { useJobPolling } from './useJobPolling';
import { Job } from '../../types';
import { TemplateEntityV1alpha1 } 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: TemplateEntityV1alpha1 | null;
};
export const JobStatusModal = ({
onClose,
jobId,
onComplete,
entity,
}: Props) => {
const job = useJobPolling(jobId);
useEffect(() => {
if (job?.status === 'COMPLETED') onComplete(job);
}, [job, onComplete]);
return (
<Dialog open onClose={onClose} fullWidth>
<DialogTitle id="responsive-dialog-title">
Creating component...
</DialogTitle>
<DialogContent>
{!job ? (
<LinearProgress />
) : (
(job?.stages ?? []).map(step => (
<JobStage
log={step.log}
name={step.name}
key={step.name}
startedAt={step.startedAt}
endedAt={step.endedAt}
status={step.status}
/>
))
)}
</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,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 { JobStatusModal } from './JobStatusModal';
@@ -0,0 +1,62 @@
/*
* 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 { useState, useEffect } from 'react';
import { Job } from '../../types';
import { useApi } from '@backstage/core';
import { scaffolderApiRef } from '../../api';
const DEFAULT_POLLING_INTERVAL = 1000;
const poll = (thunk: () => Promise<void>, ms: number) => {
let shouldStop = false;
(async () => {
while (!shouldStop) {
await thunk();
await new Promise(res => setTimeout(res, ms));
}
})();
return () => {
shouldStop = true;
};
};
export const useJobPolling = (
jobId: string | null,
pollingInterval = DEFAULT_POLLING_INTERVAL,
) => {
const scaffolderApi = useApi(scaffolderApiRef);
const [job, setJob] = useState<Job | null>(null);
useEffect(() => {
if (!jobId) return () => {};
const stopPolling = poll(async () => {
const nextJobState = await scaffolderApi.getJob(jobId);
if (
nextJobState.status === 'FAILED' ||
nextJobState.status === 'COMPLETED'
) {
stopPolling();
}
setJob(nextJobState);
}, pollingInterval);
return () => {
stopPolling();
};
}, [jobId, setJob, scaffolderApi, pollingInterval]);
return job;
};
@@ -0,0 +1,110 @@
/*
* 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 { JSONSchema } from '@backstage/catalog-model';
import { Content, StructuredMetadataTable } from '@backstage/core';
import {
Box,
Button,
Paper,
Step,
StepContent,
StepLabel,
Stepper,
Typography,
} from '@material-ui/core';
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<Omit<FormProps<any>, 'schema'>>;
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,
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, ...formProps }) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
<StepContent>
<Form
noHtml5Validate
formData={formData}
onChange={onChange}
schema={schema as FormProps<any>['schema']}
onSubmit={e => {
if (e.errors.length === 0) handleNext();
}}
{...formProps}
>
<Button disabled={activeStep === 0} onClick={handleBack}>
Back
</Button>
<Button variant="contained" color="primary" type="submit">
Next step
</Button>
</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,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 { MultistepJsonForm } from './MultistepJsonForm';
@@ -14,32 +14,44 @@
* 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 ScaffolderPage: React.FC<{}> = () => {
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,15 +108,7 @@ const ScaffolderPage: React.FC<{}> = () => {
templates.map(template => {
return (
<Grid item xs={12} sm={6} md={3}>
<TemplateCard
key={template.metadata.uid}
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>
);
})}
@@ -113,5 +117,3 @@ const ScaffolderPage: React.FC<{}> = () => {
</Page>
);
};
export default ScaffolderPage;
@@ -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';
@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { Button, Card, Chip, Typography, makeStyles } from '@material-ui/core';
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 { templateRoute } from '../../routes';
const useStyles = makeStyles(theme => ({
header: {
@@ -37,19 +40,23 @@ const useStyles = makeStyles(theme => ({
},
}));
type TemplateCardProps = {
export type TemplateCardProps = {
description: string;
tags: string[];
title: string;
type: string;
name: string;
};
const TemplateCard: FC<TemplateCardProps> = ({
export const TemplateCard = ({
description,
tags,
title,
type,
}) => {
name,
}: TemplateCardProps) => {
const classes = useStyles();
const href = generatePath(templateRoute.path, { templateName: name });
return (
<Card>
@@ -65,11 +72,11 @@ const TemplateCard: FC<TemplateCardProps> = ({
{description}
</Typography>
<div className={classes.footer}>
<Button color="primary">Choose</Button>
<Button color="primary" variant="contained" to={href}>
Choose
</Button>
</div>
</div>
</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';
@@ -0,0 +1,154 @@
/*
* 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 from 'react';
import { TemplatePage } from './TemplatePage';
import { wrapInTestApp, renderWithEffects } from '@backstage/test-utils';
import { ApiRegistry, errorApiRef, ApiProvider } from '@backstage/core';
import { scaffolderApiRef, ScaffolderApi } from '../../api';
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog';
import { mutate } from 'swr';
import { act } from 'react-dom/test-utils';
import { Route, MemoryRouter } from 'react-router';
import { rootRoute } from '../../routes';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
const templateMock = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'file:/something/sample-templates/react-ssr-template/template.yaml',
},
name: 'react-ssr-template',
title: 'React SSR Template',
description:
'Next.js application skeleton for creating isomorphic web applications.',
tags: ['Recommended', 'React'],
uid: '55efc748-4a2b-460f-9e47-3f4fd23b46f7',
etag: 'MTM3YThjY2QtYTc1MS00MTFkLTk3YTAtNzgyMDg3MDVmZTVm',
generation: 1,
},
spec: {
processor: 'cookiecutter',
type: 'website',
path: '.',
schema: {
required: ['component_id', 'description'],
properties: {
component_id: {
title: 'Name',
type: 'string',
description: 'Unique name of the component',
},
description: {
title: 'Description',
type: 'string',
description: 'Description of the component',
},
},
},
},
};
jest.mock('react-router-dom', () => {
return {
...(jest.requireActual('react-router-dom') as any),
useParams: () => ({
templateName: 'test',
}),
};
});
const scaffolderApiMock: Partial<ScaffolderApi> = {
scaffold: jest.fn(),
};
const catalogApiMock = {
getEntities: jest.fn() as jest.MockedFunction<CatalogApi['getEntities']>,
};
const errorApiMock = { post: jest.fn(), error$: jest.fn() };
const apis = ApiRegistry.from([
[scaffolderApiRef, scaffolderApiMock],
[errorApiRef, errorApiMock],
[catalogApiRef, catalogApiMock],
]);
describe('TemplatePage', () => {
afterEach(async () => {
// Cleaning up swr's cache
await act(async () => {
await mutate('templates/test');
});
});
it('renders correctly', async () => {
catalogApiMock.getEntities.mockResolvedValueOnce([templateMock]);
const rendered = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
</ApiProvider>,
),
);
expect(rendered.queryByText('Create a new component')).toBeInTheDocument();
expect(rendered.queryByText('React SSR Template')).toBeInTheDocument();
// await act(async () => await mutate('templates/test'));
});
it('renders spinner while loading', async () => {
let resolve: Function;
const promise = new Promise<any>(res => {
resolve = res;
});
catalogApiMock.getEntities.mockResolvedValueOnce(promise);
const rendered = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
</ApiProvider>,
),
);
expect(rendered.queryByText('Create a new component')).toBeInTheDocument();
expect(rendered.queryByTestId('loading-progress')).toBeInTheDocument();
// Need to cleanup the promise or will timeout
resolve!();
});
it('navigates away if no template was loaded', async () => {
catalogApiMock.getEntities.mockResolvedValueOnce([]);
const rendered = await renderWithEffects(
<ApiProvider apis={apis}>
<ThemeProvider theme={lightTheme}>
<MemoryRouter initialEntries={['/create/test']}>
<Route path="/create/test">
<TemplatePage />
</Route>
<Route path={rootRoute.path} element={<>This is root</>} />
</MemoryRouter>
</ThemeProvider>
</ApiProvider>,
);
expect(
rendered.queryByText('Create a new component'),
).not.toBeInTheDocument();
expect(rendered.queryByText('This is root')).toBeInTheDocument();
});
});
@@ -0,0 +1,182 @@
/*
* 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 { 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 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 { 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({
kind: 'Template',
'metadata.name': templateName,
}) as Promise<TemplateEntityV1alpha1[]>,
);
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 { template, loading } = useTemplate(templateName, catalogApi);
const [formState, setFormState] = useState({});
const handleFormReset = () => setFormState({});
const handleChange = (e: IChangeEvent) =>
setFormState({ ...formState, ...e.formData });
const [jobId, setJobId] = useState<string | null>(null);
const handleClose = () => setJobId(null);
const handleCreate = async () => {
const job = await scaffolderApi.scaffold(template!, formState);
setJobId(job);
};
const [entity, setEntity] = React.useState<TemplateEntityV1alpha1 | 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}.`,
),
);
return;
}
const {
entities: [createdEntity],
} = await catalogApi.addLocation('github', componentYaml);
setEntity((createdEntity as any) as TemplateEntityV1alpha1);
};
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>
<Header
pageTitleOverride="Create a new component"
title={
<>
Create a new component <Lifecycle alpha shorthand />
</>
}
subtitle="Create new software components using standard templates"
/>
<Content>
{loading && <LinearProgress data-testid="loading-progress" />}
{jobId && (
<JobStatusModal
onComplete={handleCreateComplete}
jobId={jobId}
onClose={handleClose}
entity={entity}
/>
)}
{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,
},
{
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';
+3 -1
View File
@@ -14,4 +14,6 @@
* limitations under the License.
*/
export { plugin, rootRoute } from './plugin';
export { plugin } from './plugin';
export { ScaffolderApi, scaffolderApiRef } from './api';
export { rootRoute, templateRoute } from './routes';
+5 -8
View File
@@ -14,18 +14,15 @@
* limitations under the License.
*/
import { createPlugin, createRouteRef } from '@backstage/core';
import ScaffolderPage from './components/ScaffolderPage';
export const rootRoute = createRouteRef({
icon: () => null,
path: '/create',
title: 'Create entity',
});
import { createPlugin } from '@backstage/core';
import { ScaffolderPage } from './components/ScaffolderPage';
import { TemplatePage } from './components/TemplatePage';
import { rootRoute, templateRoute } from './routes';
export const plugin = createPlugin({
id: 'scaffolder',
register({ router }) {
router.addRoute(rootRoute, ScaffolderPage);
router.addRoute(templateRoute, TemplatePage);
},
});
+27
View File
@@ -0,0 +1,27 @@
/*
* 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({
icon: () => null,
path: '/create',
title: 'Create new entity',
});
export const templateRoute = createRouteRef({
icon: () => null,
path: '/create/:templateName',
title: 'Entity creation',
});
+34
View File
@@ -0,0 +1,34 @@
/*
* 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 type Job = {
id: string;
metadata: {
entity: any;
values: any;
remoteUrl?: string;
};
status: 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED';
stages: Stage[];
error?: Error;
};
export type Stage = {
name: string;
log: string[];
status: 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED';
startedAt: string;
endedAt?: string;
};
+156 -8
View File
@@ -868,6 +868,14 @@
"@babel/plugin-transform-react-jsx-self" "^7.9.0"
"@babel/plugin-transform-react-jsx-source" "^7.9.0"
"@babel/runtime-corejs2@^7.8.7":
version "7.10.3"
resolved "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.10.3.tgz#81bc99a96bfcb6db3f0dcf73fdc577cc554d341b"
integrity sha512-enKvnR/kKFbZFgXYo165wtSA5OeiTlgsnU4jV3vpKRhfWUJjLS6dfVcjIPeRcgJbgEgdgu0I+UyBWqu6c0GumQ==
dependencies:
core-js "^2.6.5"
regenerator-runtime "^0.13.4"
"@babel/runtime-corejs3@^7.10.2":
version "7.10.3"
resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz#931ed6941d3954924a7aa967ee440e60c507b91a"
@@ -2439,6 +2447,28 @@
prop-types "^15.6.1"
react-lifecycles-compat "^3.0.4"
"@rjsf/core@^2.1.0":
version "2.1.0"
resolved "https://registry.npmjs.org/@rjsf/core/-/core-2.1.0.tgz#00130c89959850cc90224fd14c82feaecc2b9dc8"
integrity sha512-2A65RZ3I/RVVNfJUTYUkFs4snX02GHzql6o/KcLBBoG8G8+gr9ZeIi3+JEbe2mOEN02rIPnyJtBkl6C7QajyAw==
dependencies:
"@babel/runtime-corejs2" "^7.8.7"
"@types/json-schema" "^7.0.4"
ajv "^6.7.0"
core-js "^2.5.7"
json-schema-merge-allof "^0.6.0"
jsonpointer "^4.0.1"
lodash "^4.17.15"
prop-types "^15.7.2"
react-app-polyfill "^1.0.4"
react-is "^16.9.0"
shortid "^2.2.14"
"@rjsf/material-ui@^2.1.0":
version "2.1.0"
resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-2.1.0.tgz#a361b125af3a383b7f671634b8d254345f9f9311"
integrity sha512-9mMttnPNP6GiP7BtZGimYcYsbWwjyviqg/PD8oxrkEtZykULXOIdC3WDMJ3nPSym8RvZsgSnB8bajCpa5iSYQQ==
"@rollup/plugin-commonjs@^13.0.0":
version "13.0.0"
resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-13.0.0.tgz#8a1d684ba6848afe8b9e3d85649d4b2f6f7217ec"
@@ -2546,10 +2576,10 @@
eslint-plugin-react "^7.12.4"
eslint-plugin-react-hooks "^4.0.0"
"@spotify/prettier-config@^7.0.0":
version "7.0.0"
resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-7.0.0.tgz#47750979d1282197295108b6958360660a955c16"
integrity sha512-lIMcx/2oDqTtW84iHKkRJe+8U6HK6GPwWH5sJp9UEHcDpdXomOQYvwcGXy2I2zwPQQ14gYYE6nEJuSnnYqsYRw==
"@spotify/prettier-config@^8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-8.0.0.tgz#8b6c2bd579ddc54887155a0721fe04e96c89f7f2"
integrity sha512-so8w32ZV42CHWxOEXcBtbNO/hLXFrQNXVmhfzhUI6dVB9cq2xjRaiqu8GjFj8LvKbWpPj+S+KwTIS4aDVWqrFQ==
"@spotify/web-scripts-utils@^7.0.0":
version "7.0.0"
@@ -3638,7 +3668,7 @@
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
"@types/json-schema@^7.0.4":
"@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5":
version "7.0.5"
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
@@ -4516,7 +4546,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1:
resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da"
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5:
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5, ajv@^6.7.0:
version "6.12.2"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
@@ -4807,7 +4837,7 @@ arrify@^1.0.1:
resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
asap@^2.0.0, asap@~2.0.3:
asap@^2.0.0, asap@~2.0.3, asap@~2.0.6:
version "2.0.6"
resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
@@ -6377,6 +6407,25 @@ compression@^1.7.4:
safe-buffer "5.1.2"
vary "~1.1.2"
compute-gcd@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.0.tgz#fc1ede5b65001e950226502f46543863e4fea10e"
integrity sha1-/B7eW2UAHpUCJlAvRlQ4Y+T+oQ4=
dependencies:
validate.io-array "^1.0.3"
validate.io-function "^1.0.2"
validate.io-integer-array "^1.0.0"
compute-lcm@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.0.tgz#abd96d040b41b0a166f89944b5c8b7c511e21ad5"
integrity sha1-q9ltBAtBsKFm+JlEtci3xRHiGtU=
dependencies:
compute-gcd "^1.2.0"
validate.io-array "^1.0.3"
validate.io-function "^1.0.2"
validate.io-integer-array "^1.0.0"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -6626,7 +6675,7 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1:
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
core-js@^2.4.0:
core-js@^2.4.0, core-js@^2.5.7, core-js@^2.6.5:
version "2.6.11"
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
@@ -6636,6 +6685,11 @@ core-js@^3.0.1, core-js@^3.0.4:
resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647"
integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==
core-js@^3.5.0:
version "3.6.5"
resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -11606,6 +11660,22 @@ json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-bet
resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
json-schema-compare@^0.2.2:
version "0.2.2"
resolved "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz#dd601508335a90c7f4cfadb6b2e397225c908e56"
integrity sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==
dependencies:
lodash "^4.17.4"
json-schema-merge-allof@^0.6.0:
version "0.6.0"
resolved "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.6.0.tgz#64d48820fec26b228db837475ce3338936bf59a5"
integrity sha512-LEw4VMQVRceOPLuGRWcxW5orTTiR9ZAtqTAe4rQUjNADTeR81bezBVFa0MqIwp0YmHIM1KkhSjZM7o+IQhaPbQ==
dependencies:
compute-lcm "^1.1.0"
json-schema-compare "^0.2.2"
lodash "^4.17.4"
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -11616,6 +11686,11 @@ json-schema@0.2.3:
resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
json-schema@^0.2.5:
version "0.2.5"
resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.5.tgz#97997f50972dd0500214e208c407efa4b5d7063b"
integrity sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ==
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
@@ -11673,6 +11748,11 @@ jsonparse@^1.2.0:
resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
jsonpointer@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk=
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -13034,6 +13114,11 @@ nano-css@^5.2.1:
stacktrace-js "^2.0.0"
stylis "3.5.0"
nanoid@^2.1.0:
version "2.1.11"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -14950,6 +15035,13 @@ promise.series@^0.2.0:
resolved "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz#2cc7ebe959fc3a6619c04ab4dbdc9e452d864bbd"
integrity sha1-LMfr6Vn8OmYZwEq029yeRS2GS70=
promise@^8.0.3:
version "8.1.0"
resolved "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e"
integrity sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==
dependencies:
asap "~2.0.6"
promisify-node@~0.3.0:
version "0.3.0"
resolved "https://registry.npmjs.org/promisify-node/-/promisify-node-0.3.0.tgz#b4b55acf90faa7d2b8b90ca396899086c03060cf"
@@ -15160,6 +15252,13 @@ raf-schd@^4.0.2:
resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
raf@^3.4.1:
version "3.4.1"
resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
dependencies:
performance-now "^2.1.0"
ramda@0.26.1, ramda@^0.26:
version "0.26.1"
resolved "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
@@ -15238,6 +15337,18 @@ rc@^1.2.7, rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-app-polyfill@^1.0.4:
version "1.0.6"
resolved "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-1.0.6.tgz#890f8d7f2842ce6073f030b117de9130a5f385f0"
integrity sha512-OfBnObtnGgLGfweORmdZbyEz+3dgVePQBb3zipiaDsMHV1NpWm0rDFYIVXFV/AK+x4VIIfWHhrdMIeoTLyRr2g==
dependencies:
core-js "^3.5.0"
object-assign "^4.1.1"
promise "^8.0.3"
raf "^3.4.1"
regenerator-runtime "^0.13.3"
whatwg-fetch "^3.0.0"
react-beautiful-dnd@^13.0.0:
version "13.0.0"
resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40"
@@ -16718,6 +16829,13 @@ shellwords@^0.1.1:
resolved "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
shortid@^2.2.14:
version "2.2.15"
resolved "https://registry.npmjs.org/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122"
integrity sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==
dependencies:
nanoid "^2.1.0"
side-channel@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947"
@@ -18729,6 +18847,36 @@ validate-npm-package-name@^3.0.0:
dependencies:
builtins "^1.0.3"
validate.io-array@^1.0.3:
version "1.0.6"
resolved "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz#5b5a2cafd8f8b85abb2f886ba153f2d93a27774d"
integrity sha1-W1osr9j4uFq7L4hroVPy2Tond00=
validate.io-function@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz#343a19802ed3b1968269c780e558e93411c0bad7"
integrity sha1-NDoZgC7TsZaCaceA5VjpNBHAutc=
validate.io-integer-array@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz#2cabde033293a6bcbe063feafe91eaf46b13a089"
integrity sha1-LKveAzKTpry+Bj/q/pHq9GsToIk=
dependencies:
validate.io-array "^1.0.3"
validate.io-integer "^1.0.4"
validate.io-integer@^1.0.4:
version "1.0.5"
resolved "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz#168496480b95be2247ec443f2233de4f89878068"
integrity sha1-FoSWSAuVviJH7EQ/IjPeT4mHgGg=
dependencies:
validate.io-number "^1.0.3"
validate.io-number@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz#f63ffeda248bf28a67a8d48e0e3b461a1665baf8"
integrity sha1-9j/+2iSL8opnqNSODjtGGhZluvg=
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"