diff --git a/README.md b/README.md index 9aa7878cb0..983a8efea4 100644 --- a/README.md +++ b/README.md @@ -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 organization’s 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 organization’s 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_, it’s 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_, it’s a single, consistent experience that ties all your infrastructure tooling, resources, standards, owners, contributors, and administrators together in one place. ## Backstage Service Catalog (alpha) diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index 89234c0697..21e6d5b637 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -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 = ({ text, children }) => (
{text}
{children}
-) +); /* Do this instead: */ -type GoodProps = { text: string; children?: React.ReactNode; }; +type GoodProps = { text: string; children?: React.ReactNode }; const GoodComponent = ({ text, children }: GoodProps) => (
{text}
{children}
-) +); ``` ## 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. diff --git a/package.json b/package.json index 437ce36c4b..8b5a7a24c8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index e13046c33d..bc7170c93a 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -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( diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 889d9d2e02..b51dacde84 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -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" diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index f149b8c9b4..9469319b28 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -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'; diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts index d2e455145f..52951c793a 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts @@ -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(); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts index 0bef220a63..c2f071b7c3 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts @@ -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(), }); diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 29ca8bdfa3..cba6438ccb 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -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; }; + +export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; diff --git a/packages/core/src/components/SimpleStepper/SimpleStepper.tsx b/packages/core/src/components/SimpleStepper/SimpleStepper.tsx index bb2c3d640d..75fc1e5e62 100644 --- a/packages/core/src/components/SimpleStepper/SimpleStepper.tsx +++ b/packages/core/src/components/SimpleStepper/SimpleStepper.tsx @@ -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({ export interface StepperProps { elevated?: boolean; onStepChange?: (prevIndex: number, nextIndex: number) => void; + activeStep?: number; } export const SimpleStepper: FC = ({ children, elevated, onStepChange, + activeStep = 0, }) => { - const [stepIndex, setStepIndex] = useState(0); + const [stepIndex, setStepIndex] = useState(activeStep); const [stepHistory, setStepHistory] = useState([0]); + useEffect(() => { + setStepIndex(activeStep); + }, [activeStep]); + const steps: React.ReactNode[] = []; let endStep; Children.forEach(children, child => { diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 3ac8a5d103..36c86f2e96 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -40,4 +40,7 @@ export interface CatalogApi { removeEntityByUid(uid: string): Promise; } -export type AddLocationResponse = { location: Location; entities: Entity[] }; +export type AddLocationResponse = { + location: Location; + entities: Entity[]; +}; diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml index 124d1575ce..39100308bf 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml @@ -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 + diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml new file mode 100644 index 0000000000..5f479d9e47 --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml @@ -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}} diff --git a/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml b/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml index 681e8e7f51..1bc19db2db 100644 --- a/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml @@ -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 diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts index a8cfe9f3ea..191c949c22 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.test.ts @@ -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', + }, + }, + }, }, }; diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts index 0a3b556941..c39677232d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/processor.ts @@ -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, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index f8552e063f..6259023476 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -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', + }, + }, + }, }, }; }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/helpers.test.ts index 6aa940783f..c5380fa5d1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/helpers.test.ts @@ -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', + }, + }, + }, }, }; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts index a8f4b4ae99..9807c63c08 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/preparers.test.ts @@ -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', + }, + }, + }, }, }; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 47c91eaed2..abe12c94eb 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -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, diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 83a80aef56..67f491b609 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -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", diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts new file mode 100644 index 0000000000..4a0d508a2b --- /dev/null +++ b/plugins/scaffolder/src/api.ts @@ -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({ + 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, + ) { + 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()); + } +} diff --git a/plugins/scaffolder/src/components/JobStage/JobStage.tsx b/plugins/scaffolder/src/components/JobStage/JobStage.tsx new file mode 100644 index 0000000000..71b73b752c --- /dev/null +++ b/plugins/scaffolder/src/components/JobStage/JobStage.tsx @@ -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 ( + ] ?? + classes.neutral, + )} + expanded={expanded} + onChange={(_, newState) => setExpanded(newState)} + > + } + aria-controls={`panel-${name}-content`} + id={`panel-${name}-header`} + IconButtonProps={{ + className: classes.button, + }} + > + + {name} {timeElapsed && `(${timeElapsed})`} + + + + {log.length === 0 ? ( + No logs available for this step + ) : ( + }> +
+ +
+
+ )} +
+
+ ); +}; diff --git a/plugins/scaffolder/src/components/JobStage/index.ts b/plugins/scaffolder/src/components/JobStage/index.ts new file mode 100644 index 0000000000..d6d3534a88 --- /dev/null +++ b/plugins/scaffolder/src/components/JobStage/index.ts @@ -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'; diff --git a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx new file mode 100644 index 0000000000..c6ed55c92c --- /dev/null +++ b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx @@ -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 ( + + + Creating component... + + + {!job ? ( + + ) : ( + (job?.stages ?? []).map(step => ( + + )) + )} + + {entity && ( + + + + )} + + ); +}; diff --git a/plugins/scaffolder/src/components/JobStatusModal/index.ts b/plugins/scaffolder/src/components/JobStatusModal/index.ts new file mode 100644 index 0000000000..5598999fe3 --- /dev/null +++ b/plugins/scaffolder/src/components/JobStatusModal/index.ts @@ -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'; diff --git a/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts new file mode 100644 index 0000000000..9239864c03 --- /dev/null +++ b/plugins/scaffolder/src/components/JobStatusModal/useJobPolling.ts @@ -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, 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(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; +}; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx new file mode 100644 index 0000000000..f0ba720b39 --- /dev/null +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -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, 'schema'>>; + +type Props = { + /** + * Steps for the form, each contains label and form schema + */ + steps: Step[]; + formData: Record; + 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 ( + <> + + {steps.map(({ label, schema, ...formProps }) => ( + + {label} + +
['schema']} + onSubmit={e => { + if (e.errors.length === 0) handleNext(); + }} + {...formProps} + > + + +
+
+
+ ))} +
+ {activeStep === steps.length && ( + + + Review and create + + + + + + + + )} + + ); +}; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/index.ts b/plugins/scaffolder/src/components/MultistepJsonForm/index.ts new file mode 100644 index 0000000000..fa28c5803b --- /dev/null +++ b/plugins/scaffolder/src/components/MultistepJsonForm/index.ts @@ -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'; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx similarity index 83% rename from plugins/scaffolder/src/components/ScaffolderPage/index.tsx rename to plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 2f9774757b..ac13634842 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -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 ( - + ); })} @@ -113,5 +117,3 @@ const ScaffolderPage: React.FC<{}> = () => { ); }; - -export default ScaffolderPage; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.ts b/plugins/scaffolder/src/components/ScaffolderPage/index.ts new file mode 100644 index 0000000000..a28f771598 --- /dev/null +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.ts @@ -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'; diff --git a/plugins/scaffolder/src/components/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx similarity index 76% rename from plugins/scaffolder/src/components/TemplateCard.tsx rename to plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 6d47f05693..2f36e12511 100644 --- a/plugins/scaffolder/src/components/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -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 = ({ + +export const TemplateCard = ({ description, tags, title, type, -}) => { + name, +}: TemplateCardProps) => { const classes = useStyles(); + const href = generatePath(templateRoute.path, { templateName: name }); return ( @@ -65,11 +72,11 @@ const TemplateCard: FC = ({ {description}
- +
); }; - -export default TemplateCard; diff --git a/plugins/scaffolder/src/components/TemplateCard/index.ts b/plugins/scaffolder/src/components/TemplateCard/index.ts new file mode 100644 index 0000000000..2ead7d5b43 --- /dev/null +++ b/plugins/scaffolder/src/components/TemplateCard/index.ts @@ -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'; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx new file mode 100644 index 0000000000..6229a88fc3 --- /dev/null +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -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 = { + scaffold: jest.fn(), +}; + +const catalogApiMock = { + getEntities: jest.fn() as jest.MockedFunction, +}; +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( + + + , + ), + ); + + 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(res => { + resolve = res; + }); + catalogApiMock.getEntities.mockResolvedValueOnce(promise); + const rendered = await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + 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( + + + + + + + This is root} /> + + + , + ); + + expect( + rendered.queryByText('Create a new component'), + ).not.toBeInTheDocument(); + expect(rendered.queryByText('This is root')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx new file mode 100644 index 0000000000..5c949e55d9 --- /dev/null +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -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, + ); + 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(null); + const handleClose = () => setJobId(null); + + const handleCreate = async () => { + const job = await scaffolderApi.scaffold(template!, formState); + setJobId(job); + }; + + const [entity, setEntity] = React.useState( + 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 ; + } + + if (template && !template?.spec?.schema) { + errorApi.post( + new Error( + 'Template schema is corrupted, please check the template.yaml file.', + ), + ); + return ; + } + + return ( + +
+ Create a new component + + } + subtitle="Create new software components using standard templates" + /> + + {loading && } + {jobId && ( + + )} + {template && ( + + + + )} + + + ); +}; diff --git a/plugins/scaffolder/src/components/TemplatePage/index.ts b/plugins/scaffolder/src/components/TemplatePage/index.ts new file mode 100644 index 0000000000..2c038897f4 --- /dev/null +++ b/plugins/scaffolder/src/components/TemplatePage/index.ts @@ -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'; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 5b20cb0158..f0fdf5b429 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -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'; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index b2cb305e57..8c4ed5badd 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -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); }, }); diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts new file mode 100644 index 0000000000..28c77f29a6 --- /dev/null +++ b/plugins/scaffolder/src/routes.ts @@ -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', +}); diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts new file mode 100644 index 0000000000..7106444904 --- /dev/null +++ b/plugins/scaffolder/src/types.ts @@ -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; +}; diff --git a/yarn.lock b/yarn.lock index 54e9322520..ebfc19ec8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"