Merge branch 'master' of github.com:spotify/backstage into mob/techdocs-sanitize-html
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+9
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
+22
-20
@@ -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';
|
||||
+15
-8
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user